C++笔记摘要

一、基础知识:
1、C++与C的区别
1)设计思想:

  C++是面向对象,而C是面向过程的结构化编程语言。

2)语法上:

  C++具有封装性、继承、多态三种特性;
  C++相比C,增加许多类型安全的功能,比如强制类型转换;
  C++支持范式编程,如模板类、函数模板等

2、C++中static关键字的作用
1)局部静态变量:

   一般情况下,局部变量是放在栈区的,并且局部变量的生命周期是在该代码快执行完后就结束;若加了static修饰,其作用域和存储位置发生变化,要到程序结束后才会结束生命,除了代码块其他地方虽然变量存在但是使用不了。

2)全局静态变量:

  在全局变量前加上关键字static,全局变量就定义成一个全局静态变量。
  存储区: 静态存储区,整个程序运行期间一直存在;
  初始化: 未被初始化的静态全局变量会被自动初始化为0;
  作用域: 在声明它的文件之外是透明的(不可见,除非用extren关键字),准确是从定义之处开始,到文件结尾。

3)修饰函数:

  和修饰全局变量基本一致,就是改变了函数的作用域;

4)C++的static

  若类中某个函数为static,表明该函数属于类并非属于任何一个类的对象;若成员变量为static,则说明该变量属于类和类对象,只保存一次副本,可以通过类和对象进行访问。

3、C++中四种类型转换
1)const_cast:

  用于将const变量转换为非const;

2)static_cast:

  用于各种隐式转换,如非const转const,void*转指针等;static_cast多用于多态向上转化,如果向下转能成功但是不安全,结果也未知。

3)dynamic_cast:

  用于动态类型转换。只能用于含有虚函数的类,用于类层次之间的向上和向下转换;只能转指针或引用。向下转换时,如果是非法的对于指针返回NULL,对于引用抛异常。
   - 向上转换:子类向基类的转换;
   - 向下转换:基类向子类的转换;

4)reinterpret_cast:

  几乎什么都可以转,比如int转int*,可能会出问题,尽量少用。

思考:为什么不使用C的强制类型转换

  C的强制类型转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

4、指针和引用的区别

  1) 指针有自己的一块空间(存储的内容为对象地址),其sizeof大小为一个int4;而引用只是一个别名,其大小是被引用对象的大小;
  2) 指针可以初始化为NULL;而引用必须被初始化且必须是一个已有对象的引用;
  3) 指针需要被解引用才可以对对象进行操作,而引用直接修改即可,两则都会修改原有对象
  4) 指针可以多级指针(**p),而引用只有一级;
  5) 指针和引用运算符++的意义不一样;
  6) 若要返回动态内存分配的对象或者内存地址,必须使用指针,引用可能会引起内存泄漏。

补充:const的含义与机制:

const名叫常量限定符,用来限定特定变量,以通知编译器该变量是不可修改的。习惯性的使用const,可以避免在函数中对某些不应修改的变量造成可能的改动:
1)修饰一般常量和数组: const放在类型前或者后是等效的,说明该变量或数值内容是常量,不允许修改;
2)修改指针变量*及引用&: 若const位于*的左侧,说明是常量,指针指向的内容或者引用内容不可修改;若在右侧,const修饰的是指针或者引用,指针指向或者引用不可更改,内容可以改。
3)const修改函数的参数: 说明在函数体中不能修改其内容
4)const修改函数返回值: 说明保护返回值不被更改
5)const在类中的用法: 不能在类声明中初始化const数据成员。正确的使用const实现方法为:const数据成员的初始化只能在类构造函数的初始化表中进行类中的成员函数:A fun4()const; 其意义上是不能修改所在类的的任何变量。
6)修饰类对象: 表示常量对象,常量对象只能调用常量函数,别的成员函数都不能调用。

5、include投文件的顺序以及双引号""和尖括号<>的区别

   ** 1)** include顺序: 若在文件a.h中声明一个在文件中b.h中定义的变量,而不引用b.h,那么要在a.c文件中引用b.h文件,并且要先引用b.h,后引用a.h,否则报变量类型未声明错误。
   ** 2)** 双引号和尖括号的区别: 编译器预处理阶段查找头文件的路径不一样:
    ----双引号:当前头文件目录–>编译器设置的头文件路径(编译的时候可以通过-l显示指定搜索路径)–>系统环境变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径;
    ----尖括号:编译器设置的头文件路径(-l指定)–>系统环境变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径。

6、数组和指针的区别

  **1)指针:**保存数据的存储单元地址,间接访问数据,先获取指针的内容,然后再寻址提取里面的内容。通常用于动态的数据结构,通过Malloc分配内存,free释放内存。
  **2)数组:**直接访问数据,通过用于固定数目且数据类型相同的元素隐式的分配和删除。

7、野指针

  野指针就是指向一个已删除的对象或者申请访问受限内存区域的指针。

8、四个智能指针
为什么使用引入智能指针:

  智能指针的作用是管理指针。引入原因:当申请的空间在函数结束时忘记释放,会造成内存泄漏。使用智能指针可以很大程度上避免上面这种情况,因为智能指针本质是一个类,当超出了类的作用域。类会自动调用析构函数,析构函数会自动释放资源。因此引入智能指针的作用就是在函数结束是自动释放内存空间,不需要手动释放

1)auto_ptr(C++98的方案,C++11已经弃用):

   采用所有权模式:

auto_ptr<string> p1 (new string("I love China!"));
auto_ptr<string> p2;
p2=p1;// auto_pt不会报Error
std::cout<<*p1<<std::endl; // Error

此时运行代码不会保错,但是p2剥夺了p1的所有权,而p1成了野指针。若程序运行时访问p1将会报错。因此auto_ptr的缺点是:存在潜在的内存崩溃问题

2)unique_ptr(替换auto_ptr):

   unique_ptr实现独占式拥有和严格拥有概率,保证同一时间内只用一个智能指针可以指向该对象。它可以避免资源泄漏(例如以new创建对象后因为发生异常而忘记delete)
&emps;&emps;采用所有权模式

unique_ptr<string> p3 (new string("auto!"));
unique_ptr<string> p4 = p3;// not allowed , Error

编译器认为p4=p3非法,避免了p3不再指向无效数据的问题,因此比auto_ptr更安全;
另外,unique_ptr还有更值得亮点的地方:当程序试图将一个unique_ptr赋值给另一个时,如果unique_ptr是个临时右值(#2),编译器允许这么做;如果源unique_ptr将存一段时间,就禁止这种操作如(#1)

unique_ptr<string> pu1 (new string("hello word!"));
unique_ptr<string> pu2;
pu2=pu1; // #1 not allowd
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string("Me!")) // #2 allowd

其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。
Warning: 若想实现#1赋值,也可以实现,具体见如下代码。C++具有一个标准库函数std::move(),能够将一个unique_ptr赋值给另外一个。

unique_ptr<string> ps1,ps2;
ps1 = unique_ptr<string>(new string("C++"));
ps2 = std::move(ps1); // allowed
ps1 = unique_ptr<string>(new string("Plus"));
std::cout<<*ps1<<" "<<*ps2<<std::endl;
3)shared_ptr(解决auto_ptr独占):

   shared_ptr实现共享式 拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在最后一个引用被销毁 时候释放。从名字share就可以了解到资源可以被多个指针共享,它使用技术机制来表示资源被几个指针共享,可以通过成员函数use_count()来查当前资源的所有者个数。除了可以通过new来构造外,还可以通过传入auto_ptr、unique_ptr、weak_ptr来构造。当调用release()时,当前指针会释放资源所有权,计数减1,当计数为0时,资源会被释放。
相关成员函数说明:
   ---- use_count():返回引用计数的个数
   ---- unique():返回是否是独占所有权(use_count()是为1)
   ---- swap():交换两个share_ptr对象
   ---- reset():放弃内部对象的所有权或拥有对象的变更,会引起原有对象的引用计数的减少
   ---- get():返回内部对象(指针),由于已经重载了shapred_ptr()方法,因此和直接使用对象是一样,如下代码:

void testSmartPoint() {
	std::shared_ptr< int> sh_pt(new int(1));
	std::cout <<"sh_pt:"<< sh_pt << std::endl;
	std::cout << "sh_pt.get():"<<sh_pt.get() << std::endl;
}

在这里插入图片描述

4)weak_ptr:

   weak_ptr是一种不控制对象生命周期 的智能指针,它指向一个shared_ptr管理的对象,进行该对象的内存管理的是那个强引用的。shared_ptr.weak_ptr()只是提供了对管理对象的一个访问手段。weak_ptr设计的目的是为配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只可以从一个shared_ptr或另一个weak_ptr对象构造,它的构造函数和析构函数不会引起引用计数的增加或减少。 weak_ptr是用来解决shared_ptr相互引用时产生死锁问题。如果说两个shared_ptr相互引用,那么两个指针的引用计数永远不可能下降为0,资源永不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以进相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

class B;
class A {
	public:
		std::shared_ptr<B> _pb;
		~A() {
			std::cout << "A delete" << std::endl;
		}
};
class B {
	public:
		std::shared_ptr<A> _pa;
		~B() {
			std::cout << "B delete" << std::endl;
		}
};
void testSmartPoint() {

	std::shared_ptr<B> pb(new B());
	std::shared_ptr<A> pa(new A());
	// 相互引用
	pb->_pa = pa;
	pa->_pb = pb;
	std::cout << pb.use_count() << std::endl; 
	std::cout << pa.use_count() << std::endl; 
}
int main(int argc, char* argv[]) {
	testSmartPoint();
}

在这里插入图片描述
   在testSmartPoint函数中,pa,pb之间相互引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源未被释放(A和B的析构函数均没有被调用)。如果把其中的一个改为weak_ptr就可以了,把类A里面的shared_ptr pb;改为weak_ptr pb;运行结果如下所示。这样的话,资源B的引用开始就只有1,当析构pb时,B的计数变为0,B得到释放,那么会使A的计数减一,同时pa析构时使A的计数减1,那么A的计数为0,A就得到释放。
在这里插入图片描述
   注意不能通过weak_ptr直接访问对象的方法,比如B对象中有一个方法print(),则pa->pb->print()是不行的,会报错。pb是一个weak_ptr,应先转换为shared_ptr,在进行访问:shared_ptr<\B> p = pa->pb.lock(); p->print();

9、c++ string和c string的区别

  C语言字符串以字符数组的形式存储,以 ‘\0’ 结尾,而C++引入了string类作为字符串类型,对应的方法在头文件在中,并没有 ‘\0’ 作为结尾标志,并且还支持许多C无法支持的直接操作,如==、+=(字符串拼接)等。

const std::string const_str = "123";
	std::string st = "avcg"; // 隐式转换,const char* 转换为 string类型
	std::cout << "const string str:" << const_str.at(const_str.length() ) << std::endl; // #1 报错
	std::cout << "string str:" << st.at(st.length()) << std::endl;                      // #2 报错
	std::cout << "const string str:" << const_str.at(const_str.length()-1) << std::endl; // #3
	std::cout << "string str:" << st.at(st.length()-1) << std::endl;                    // #4

   两者可以互相转换。C++中的从C string 可以隐式转换到C++ string,反过来就不行。

	char str[] = { "12535" };
	std::string con_str(str); // #1 C++自带从c string(char str[]) 到string类型的隐式转换,反过来不支持,需要string的c_str()函数
	std::cout << con_str << std::endl;
	char* s = con_str;        // #2 Error,不能将string 直接转换为c string即char str[]
	char* s = con_str.c_str;  // #3 要借助c_str函数
9、在main()函数执行前后执行函数

   主要有两种情况:
     ---- _attribute((constructor)) 、全局静态变量的初始化都是在程序初始阶段,先于main函数的执行;
     ---- _attribute((destructor)) 标记函数应当在程序结束之前(main函数结束之后,或者调用exit()之后)执行。

在这里插入代码片
10、智能指针有没有出现内存泄漏情况与及解决办法

   会出现。因为shared_ptr互相引用时则会导致计数永远为1,无法调用相应的析构函数,释放内存。引入weak_ptr, weak_ptr的构造函数不会修改引用计数的值,从而不会对对象内存进行管理,其类似一个普通指针,却不指向引用计数的共享内存,另外其可以检测到所管理的对象是否已经被释放,从而避免非法访问。

11、为甚么会基类的析构函数必须是虚函数,而C++默认的析构函数是非虚函数

  将可能被继承的类(基类)的析构函数设置为虚函数,可以保证当new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间(会调用子类的析构函数),防止内存泄漏。
  C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。 而对于不会被继承的类(普通类)而言,其虚构函数假设是虚函数,就会浪费一定的内存。因此C++默认的析构函数不是虚函数,只有当需要作为基类的时候,才设置为虚函数。

补充:虚函数的原理和作用:

1) 原理: 虚函数表、虚函数指针
2)作用: 当调用一个虚函数时,被执行的代码必须和调用函数的对象的动态类型相一致。编译器需要做的就是如何高效的实现提供这种特性。不同编译器实现细节也不相同。大多数编译器通过** vtbl(virtual table)和vptr(virtual table pointer)来实现的。 当一个类声明了虚函数或者继承了虚函数,这个类就会有自己的vtbl**。vtbl实际上就是一个函数指针数组,有的编译器用的是链表,不过方法都是差不多。vtbl数组中的每一个元素对应一个函数指针指向该类的一个虚函数 ,同时该类的每一个对象都会包含一个vptrvptr指向该vtbl的地址
总结:

1)每个声明了虚函数或者继承了虚函数的类,都会有一个自己的vtbl
2)同时该类的每个对象都会包含一个vptr去指向该vtbl
3)虚函数按照其声明顺序放于vtbl表中, vtbl数组中的每一个元素对应一个函数指针指向该类的虚函数
4)如果子类覆盖了父类的虚函数,将被放到了虚表中原来父类虚函数的位置
5)在多继承的情况下,每个父类都有自己的虚表。子类的成员函数被放到了第一个父类的表中

衍生问题1:为什么 C++里访问虚函数比访问普通函数慢?
   单继承时性能差不多,多继承的时候会慢,从性能和空间方面分析:

性能:(从虚函数调用过程分析)
   通过对象的vptr 找到类的vtbl,这是一个简单的操作,因为编译器知道在对象内 哪里能找到 vptr(毕竟是由编译器放置的它们)。因此这个代价只是一个偏移调整(以得到 vptr)和一个指针的间接寻址(以得到 vtbl)。找到对应 vtbl 内的指向被调用函数的指针。这也是很简单的, 因为编译器为每个虚函数在 vtbl 内分配了一个唯一的索引。这步的代价只是在 vtbl 数组内 的一个偏移。调用第二步找到的的指针所指向的函数。在单继承的情况下,调用虚函数所需的代价基本上和非虚函数效率一样,在大多数计算机上它多执行了很少的一些指令,所以有很多人一概而论说虚函数性能不行是不太科学的。在多继承 的情况下,由于会根据多个父类生成多个vptr在对象里为寻找 vptr 而进行的偏移量计算会变得复杂一些,但这些并不是虚函数的性能瓶颈虚函数运行时所需的代价主要是虚函数不能是内联函 。这也是非常好理解的,是因为内联函数是指在编译期间 用被调用的函数体本身来代替函数调用的指令,但是虚函数的“虚”是指“直到运行时 才能知道要调用的是哪一个函数。”但虚函数的运行时多态特性就是要在运行时才知道具体调用哪个虚函数,所以没法在编译时进行内联函数展开。当然如果通过对象直接调用虚函数它是可以被内联,但是大多数虚函数是通过对象的指针或引用被调用的,这种调用不能被内联。 因为这种调用是标准的调用方式,所以虚函数实际上不能被内联。
内存:
在上面的虚函数实现原理部分,可以看到为了实现运行时多态机制,编译器会给每一个包含虚函数或继承了虚函数的类自动建立一个虚函数表,所以虚函数的一个代价就是会增加类的体积。在虚函数接口较少的类中这个代价并不明显,虚函数表vtbl的体积相当于几个函数指针的体积,如果你有大量的类或者在每个类中有大量的虚函数,你会发现 vtbl 会占用大量的地址空间。但这并不是最主要的代价,主要的代价是发生在类的继承过程中,在上面的分析中,可以看到,当子类继承父类的虚函数时,子类会有自己的vtbl,如果子类只覆盖父类的一两个虚函数接口,子类vtbl的其余部分内容会与父类重复。这在如果存在大量的子类继承,且重写父类的虚函数接口只占总数的一小部分的情况下,会造成大量地址空间浪费。在一些GUI库上这种大量子类继承自同一父类且只覆盖其中一两个虚函数的情况是经常有的,这样就导致UI库的占用内存明显变大。 由于虚函数指针vptr的存在,虚函数也会增加该类的每个对象的体积。在单继承或没有继承的情况下,类的每个对象会多一个vptr指针的体积,也就是4个字节;在多继承的情况下,类的每个对象会多N个(N=包含虚函数的父类个数)vptr的体积,也就是4N个字节 。当一个类的对象体积较大时,这个代价不是很明显,但当一个类的对象很轻量的时候,如成员变量只有4个字节,那么再加上4(或4N)个字节的vptr,对象的体积相当于翻了1(或N)倍,这个代价是非常大的。

衍生问题2:内联函数、构造函数、静态成员函数可以是virtual?

inline, static, constructor三种函数都不能带有virtual关键字。
inline是编译时展开,必须有实体;
static属于class自己的,也必须有实体;
virtual函数基于vtable(内存空间),constructor函数如果是virtual的,调用时也需要根据vtable寻找,但是constructor是virtual的情况下是找不到的,因为constructor自己本身都不存在了,创建不到class的实例,没有实例,class的成员(除了public static/protected static for friend class/functions,其余无论是否virtual)都不能被访问了。
虚函数实际上不能被内联:虚函数运行时所需的代价主要是虚函数不能是内联函。
构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还没有构造好, 多态是被disable的。
静态的对象是属于整个类的,不对某一个对象而言,同时其函数的指针存放也不同于一般的成员函数,其无法成为一个对象的虚函数的指针以实现由此带来的动态机制

12、静态函数和虚函数的区别

   静态函数在编译的时候就已经确定了运行时机,虚函数在运行的时候才动态绑定。虚函数使用了虚函数表机制,调用的时候会增加一次内存开销。

13、虚函数、纯虚函数、抽象类联系与区别

   首先有以下几点认识:
   ---- 定义一个函数为虚函数,不代表函数为不被实现的函数(如在基类中可以有相应的实现);
   ---- 定义一个函数为虚函数,是为了允许使用基类的指针来调用子类的这个函数;
   ---- 定义一个函数为纯虚函数,才代表函数没有被实现;
   ---- 定义一个函数为纯虚函数,是为了实现一个接口,起到一个规范的作用,子类必须实现这些函数。
例如以下代码:

#include<iostream>
// 测试虚函数
class A {
public:
	virtual void printInfor() {
		std::cout << "A print--------------" << std::endl;
	}
};
class B :public A {
public:
	void printInfor() {
		std::cout << "B print---------------" << std::endl;
	}
};
int main(int argc, char** argv) {
	A *a = new B();// 基类指针指向子类对象
	a->printInfor(); // 虽然a是指向A的指针,但调用的函数确实子类的
	return 0;
}

在这里插入图片描述
通过上面例子,虚函数所谓的虚是指动态联编,在运行的时候才绑定函数,在编译的时候不绑定。
纯虚函数:
   纯虚函数是在基类中声明的虚函数,它在基类中没有定义和实现,因此要派生类中一定要有定义和实现。在基类中定义纯虚函数是在虚函数后面加=0,如:virtual void printfInfor() = 0;
引入纯虚函数原因: 很多时候基类本身生成对象是不合理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
   最显著特征:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。

抽象类:
   (1)、定义:称带有纯虚函数的类为抽象类
   (2)、作用:抽象类的主要作用将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。
   (3)、注意:抽象类只能作为基类使用,其纯虚函数的定义和实现都有派生类给出,只是声明而已。若派生类中没有被重新定义纯虚函数,而只是继承基类的纯虚函数,则派生类仍然是一个抽象类。若派生类给出了基类纯虚函数的定义和实现,则该派生类就不再是抽象类,它是一个可以通过new建立实例的类,抽象类不能有实例化对象。

14、析构函数的作用

&emps; 析构函数和构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。析构函数函数名和类名一致,只是在函数名前面加一个~,以区别构造函数。它不能带任何参数,也无返回值。只能有一个析构函数,不能重载。若用于没有编写析构函数,编译系统会自动生成一个缺省的析构函数;若自定义了析构函数,编译器也总是会为我们合成一个析构函数,编译器在执行的时候回先调用自定义的析构函数在调用合成的析构函数)。它也不会进行任何操作,因此一般许多类没有显示定义析构函数。
  执行顺序:派生类本身的析构函数-----》对象成员析构函数---------》基类析构函数

15、重载、覆盖(重写)、隐藏区别

   (1) 重载: 同一访问区域内(同类)、函数名相同、函数参数列表不同(不关心返回值)、virtual关键字可有可无。
   (2) 覆盖: 派生类覆盖基类函数,特点 不同访问区域(不同类)、函数名相同、参数列表相同、基类函数必须有virtual关键字。
   (3) 隐藏: 隐藏是指派生类对象指针只能调用自己的同名函数,不可调用基类的同名函数 。
    a、派生类的函数和基类函数同名,但参数不同无论是否有virtual关键字,基类的函数被隐藏
    b、派生类的函数和基类函数同名,并且参数也同但是基类无virtual关键字,基类的函数被隐藏

#include<iostream>
// 测试虚函数
class Base {
public :
	virtual void A(int i) {
		std::cout << "Base::A()" << std::endl;
	}
	void B(int i) {
		std::cout << "Base::B():" <<i<< std::endl;
	}
	void C(int i) {
		std::cout << "Base::C()" << std::endl;
	}
	void D() {
		std::cout << "Base::D()" << std::endl;
	}
};
class Derive : public Base {
public:
	// 覆盖父类A 函数名、参数完全相同,且父类要有virtual,子类virtual 可有可无
	virtual void A(int i) {
		std::cout << "Derive::A():"<<i << std::endl;
	}
	// 隐藏父类B 函数名、参数完全相同,父类无virtual(隐藏了就是子类不能调用父类中的B函数)
	void B(int i) {
		std::cout << "Derive::B():"<<i << std::endl;
	}
	// 隐藏父类中的C 函数相同、参数不同,与virtual无关 (隐藏了就是子类不能调用父类中的C函数)
	void C(const char* str) {
		std::cout << "Derive::C()" << std::endl;
	}
};
int main(int argc, char** argv) {
	Derive *de = new Derive();
	Base *ba = de;
	// 覆盖了,均调用Derive 中的A()
	de->A(1);
	ba->A(2);
	// 分别调用自己的函数
	de->B(1);
	ba->B(2);
	// 分别调用自己的函数
	de->C("1");
	ba->C(1);
	//de->C(1); #1 Error 由于隐藏了父类中的void C(int i).所以不可调用
	de->D();
	
}

在这里插入图片描述
覆盖是在程序运行时动态绑定,实现面向对象多态特性。重载和隐藏是在编译时进行绑定,静态多态。

16、C++是如何定义常量,常量存放在内存的哪个位置

&emps;&emps; 常量在C++里的定义就是一个top-level-const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区;对于全局对象,常量放在全局/静态存储区。对于字面常量,存放在常量存储区

17、说一说extern “C”

   C++调用C函数需要extern C,因为C语言没有函数重载。

补充:extern

   在C语言中,extern用在变量或者函数的声明前,用来说明“此变量/函数是在别处定义的,要在此处引用”。注意:
1)注意extern声明的位置对其作用域也有关系,如果是在main函数中进行声明的,则只能在main函数中调用,在其它函数中不能调用。
2)在C++中extern还有另外一种作用,用于指示C或者C++函数的调用规范。比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同,用此来解决名字匹配的问题。
问题:为什么要用到其他文件变量,不直接include头文件,要用extern关键字?
   因为用extern会加速程序的编译过程,这样能节省时间。

补充:宏定义和展开、内联函数区别:

1)、内联函数:代码被插入到调用者代码处的函数,就如同宏定义:#define 宏。内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。

  • a、宏定义不检查函数参数,返回值什么的,只是展开,相对来说,内联函数会检查参数类型,所以更安全。
  • b、宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。
  • c、内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样的展开,所以取消了函数的参数压栈,减少了调用的开销
    问题:有了函数还需要内联和宏?
  • a、函数并不能完全替代宏,有些宏可以在当前作用域生成一些变量,函数做不到。
  • b、内联函数只是函数的一种,内联是给编译器的提示,告诉它最好把这个函数在被调用处展开,省掉一个函数调用的开销(压栈,跳转,返回)
  • c、内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样
18、new/delete与malloc/free的区别

   new/delete是C++的关键字,而malloc/free是C语言的库函数,后者必须指明申请内存空间的大小,对于类类型的对象,后者不会调用投资函数和析构函数,前者会。

二、容器和算法:
1、list、vector、deque、map、set区别和用法比较
1)vector:数组

   向量,相当于一个数组。在内存中分配一块连续存储 空间。STL内部实现时,首先分配一个非常大的内存空间预备进行存储,即capacity()函数返回的大小,当超过次分配的空间时整体会重新分配一块内存,因此vector可以给人不需要指定大小的感觉。
   优点:
    ------ 不指定一块内存大小的连续存储空间,可以像数组一样操作,但还可以进行动态操作。
    ------ 随机访问方便,支持[]和at()
    ------节省空间
   缺点:
    ------ 在内部进行插入删除操作效率低
    ------ 只能在vector的最后进行push和pop
    ------ 当动态添加的数据超出vector默认分配的大小时要重新进行分配,这会进行拷贝、释放操作

2)list:双向链表

   每一个节点都包括一个信息快info、一个前驱指针Pre、一个后驱指针Post。可以不分配内存大小进行动态添加和删除,使用的是非连续内存空间进行存储。
   优点:
    ------ 使用非连续内存完成动态操作
    ------ 在内部快捷方便的进行插入和删除操作
    ------ 可以在两端进行push和pop操作
   缺点:
    ------ 不能随机访问
    ------ 相对于vector占内存多

3)deque:双端队列

   功能上合并了vector和list
   优点:
    ------ 随机访问方便,即支持[]操作符和at()
    ------ 在内部方便的进行插入和删除操作
    ------ 可在两端进行push、pop
   缺点:
    ------ 占用内存过大
三者使用总结:
    ------ 高效的随机存取,不在乎插入和删除效率,选择vector;
    ------ 大量的插入和删除,不在乎随机存取,选择list;
    ------ 需要随机存取、且还关心两端数据的插入和删除,选择deque;

4)map:内部一颗红黑树(严格意义上的平衡二叉树)

   map是STL的一个关联容器,它提供一对一的数据处理能力,以键值对形式存储(第一个是关键字key,第二个是值value)。map内部自建一颗红黑树,对数据具有自动排序的功能,因此map内部数据是有序的。使用STL的头文件没有扩展名.h.
   数据的插入:
    ------ object.insert(pair<keyType, valueType>(key, value))
    ------ object.insert(std::map<keyType, valueType>::value_type(key, value))
    ------ object[key] = value
以上三种方案,前两种方式在前后插入相同的key时,第二次插入会失败;第三种方案会直接覆盖第一次的内容。
   数据的移除erase()三个重载:
    ------ iterator erase(iterator it) // 通过迭代器指向删除
    ------ iterator erase(iterator first, iterator last) // 通过迭代器删除一个范围
    ------ size_type erase(const Key&key) // 通过关键字删除
    ------ clear()相当于erase(object.begin(),object.end());
   map中的sort问题:
&emps;&emps; map中的元素是自动按Key升序排序,因此不能多map使用sort函数;STL中默认是采用小于号排序的,若关键字为int型,它本身支持小于号操作,因此是可以从小到大排序;若关键字是一个结构体,涉及到排序就会出现问题,因为结构体没有小于号,insert等函数在编译的时候过不去,可以有以下两种方式解决:
   a、小于号重载:

#include <iostream>
#include <map>
#include <string>
#include <vector>
using namespace std;
typedef struct studentInfo {
public:
	int stu_id;
	string stu_name;
	bool operator<(studentInfo const& stu) const {
		if (stu_id < stu.stu_id) // 第一原则:按照学生的id从小到大排序
			return true;
		if (stu_id == stu.stu_id)// 第二原则:若id一致,按照学生的名字顺序排序
			return stu_name.compare(stu.stu_name) < 0;
		return false;
	}
}StudentInfor;

int main(int argc, char** argv) {
	map<StudentInfor, int> map_stu;
	vector<string> names = {"one_stu","two_stu","third_stu"}; // 名字
	vector<int>  ids = { 2,1,3 }; // 学号
	vector<int>  scores = { 90,70,100 }; // 分数
	for (int i = 0; i < names.size();i++)
	{
		//StudentInfor* p = (StudentInfor*) malloc(sizeof(StudentInfor));
		StudentInfor *p = new StudentInfor;
		p->stu_name = names[i];
		p->stu_id = ids[i];
		map_stu.insert(pair<StudentInfor, int>(*p,scores[i]));
	}
	map<StudentInfor, int>::iterator iter;
	for (iter = map_stu.begin(); iter != map_stu.end(); iter++)
		cout << "学号:" << iter->first.stu_id << " 姓名:" << iter->first.stu_name << "  分数:" << iter->second << endl;
	return 0;
}

在这里插入图片描述
温馨提示:若在结构体中出现string 类型的成员变量,要转换为char[] 的形式,然后通过strcpy赋值,因为结构体中若使用malloc进行内存申请不会调用构造函数,string类型不定长不会分配内存,因此直接=赋值会报错;另外若想保留string类型,可以改用new来申请,这样的会可以直接通过=赋值。
   b、仿函数的应用,在结构体中没有直接进行小于号重载:

typedef struct test_infor {
	int stu_id;
	string stu_name;
}StudentInfor;
class sort {
public:
	bool operator()(StudentInfor const &stu_a, StudentInfor const &stu_b) {
		if (stu_a.stu_id < stu_b.stu_id)
			return true;
		if(stu_a.stu_id == stu_b.stu_id)
			return stu_a.stu_name.compare(stu_b.stu_name) < 0;
		return false;
	}
};

int main(int argc, char** argv) {
	map<StudentInfor, int,sort> map_stu;
	vector<string> names = {"one_stu","two_stu","third_stu"}; // 名字
	vector<int>  ids = { 1,1,3 }; // 学号
	vector<int>  scores = { 90,70,100 }; // 分数
	for (int i = 0; i < names.size();i++)
	{
		//StudentInfor* p = (StudentInfor*) malloc(sizeof(StudentInfor));
		StudentInfor *p = new StudentInfor;
		p->stu_name = names[i];
		p->stu_id = ids[i];
		map_stu.insert(pair<StudentInfor, int>(*p,scores[i]));
	}
	map<StudentInfor, int>::iterator iter;
	for (iter = map_stu.begin(); iter != map_stu.end(); iter++)
		cout << "学号:" << iter->first.stu_id << " 姓名:" << iter->first.stu_name << "  分数:" << iter->second << endl;
	return 0;
}

由于STL是一个统一的整体,map的很多用法都和STL中其它的东西结合在一起

5)set:内部一颗红黑树(严格意义上的平衡二叉树)

这部分省略。

2、STL中的allocaotr:

   STL的分配器用于封装STL容器在内存管理上的底层细节。在C++中,其内存配置和释放描述如下:
     ----- new运算分为两阶段:调用::operator new 配置内存----------》调用对象构造函数构造对象内容
     ----- delete运算分为两阶段:调用对象析构函数----------》调用::operator delete释放内存
为了精密分工,STL allocator将两个阶段操作分开:内存配置alloc::allocate负责,释放alloc::deallocate()负责;对象构造::construct()负责,对象析构::destroy()负责。同时为了提升内存的管理效率,减少申请小内存造成的内存碎片问题,SGI STL采用了来两级配置器,当分配的空间大小超过128B时,使用第第一级空间配置;当分配空间的大小小于128B时,将使用第二级空间配置器。第一级直接使用malloc()、realloc()、free()函数进行内存空间的分配和释放,而第二级采用了内存池技术,通过空链表来管理内存。

2、STL基本组成:

   主要由容器、迭代器、仿函数、算法、分配器、配接器。它们之间的关系:
1)分配器给容器分配存储空间;
2)算法通过迭代器获取容器的内容;
3)仿函数可以协助算法完成各种操作;
4)配接器用来套接适配仿函数。

3、STL迭代器删除元素:

   主要考察迭代器失效 的问题:
1)序列容器vector、deque: 使用erase(iteror)后,后边每个元素的迭代器都会失效。因为后边每个元素都会往前移动一格位置,当前的迭代指针指向下一个元素。(错位)
2)关联容器map、set: 使用erase(iterator)后,当前元素的迭代器失效,但是其结构是红黑树,删除当前元素,不会影响下一个元素的迭代器,因此只需要在调用erase前,记录下一个元素的迭代器即可。
3)list: 它使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的ierator。

4、STL中迭代器的作用,有指针为何还需要迭代器:

1) 迭代器:
   iterator模式又称为Cursor模式,用于提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部形式。换句话,iterator模式用于聚合对象的一种模式,使得我们可以在不知道对象内部的情况下,按照一定顺序访问聚合对象中的各个元素。
2) 和指针的区别:
   迭代器不是指针,而是类模板,表现的像指针。它模拟了指针的功能,通过重载了指针的操作符如->、*、++、–等。迭代器封装了原生指针,是指针概念的一种提升,提供了比指针更高级的行为,相当于一种智能指针,可以根据不同的数据类型来实现不同的++、–等操作。
3) 迭代器产生原因:
   Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。

三、类和数据抽象:
1、C++中类成员的访问权限:

   通过public、protected、private三个关键字控制成员变量和函数的访问权限。
1)在类的内部都可以互相访问;
2)在类的外部,只能通过对象访问public属性的成员,不能访问private、protected属性成员。

2、C++中struct和class

在C++中,可以用struct和class定义类,都可以继承。区别在于struct默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private。另外,class还可以定义模板类形参,比如template <class T, int i>。

3、C++类可以定义引用数据成员?

可以,必须通过成员函数初始化列表初始化。

四、面向对象与泛型编程:
1、什么是右值引用,跟左值有什么区别:

右值引用是C++11中引入的新特性 , 它实现了转移语义和精确传递。
1) 目的:
   a、消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
   b、能够更简洁明确地定义泛型函数
2) 概念:
   左值:能对表达式取地址、或具名对象/变量。一般指表达式结束后依然存在的持久对象
   右值:不能对表达式取地址,或匿名对象。一般指表达式结束就不再存在的临时对象
3) 区别:
   a、左值可以寻址,而右值不可以。
   b、左值可以被赋值,右值不可以被赋值,可以用来给左值赋值。
   c、左值可变,右值不可变(仅对基础类型适用,用户自定义类型右值引用可以通过成员函数改变)。

补充设计模式:
1、单例模式

   确保一个类只有一个实例,并且这个实例化向整个系统提供。又可以分为以下两类:
1)饿汉模式:程序运行即创建对象并实例化,静态实现所以是线程安全的;
2)懒汉模式 :创建对象不实例化,需要的时候才实例化,线程不安全需要加锁

2、工厂模式

   建立对象的类就如一个加工厂,建立对象就如一个产品,使用产品而不用在乎产品如何生成,具体可以分为以下三种:
1)简单工厂模式: 即根据工厂里的对象决定实例化什么产品(一个工厂生产两种产品)
2)工厂方法模式: 定义一个创建对象的接口(两个工厂,一个工厂只生产一种产品)
3)抽象工厂模式: 提供一个创建相互依赖对象的接口,而无需指定它们具体的类。(两个工厂,每个工厂可以产两种产品)

3、策略模式

   即每一个类都是一个锦囊,但是锦囊之间可以替代(即功能一样,实现的方法不一样)

4、适配器模式

   把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口原因不匹配而无法一起工作的两个类能够一起工作。

5、合成模式

   将对象组织到树结构中,用来描述整体与部分的关系

6、解释器模式

   给定一个语言后,解释器模式可以定义出其文法的一种表示,并同时提供一个解释器。客户端可以使用这个解释器来解释这个语言中的句子。

7、桥接模式

   将抽象化与实现化脱离,使得二者可以独立的变化。

8、观察者模式

   定义一对多的依赖关系,让多个观察者对象同时监听一个主对象。当这个主对象的动态发生变化时,会通知所有观察者对象,让其更新自己。(例如订阅邮件)

9、建造者模式

  产品内部表象可以变化,客户不必只要内部组成,可以强制执行一种分步骤进行的建造过程。用一个接口完成不同的操作。(例如登录QQ,自动选择所在地区的服务器)

10、访问者模式

   对于数据结构相对未确定的系统,把数据结构和基于数据结构的操作解耦合,让操作集合可以自由演化。(做任何更改不需要修改基类)
设计模式C++实现可参考此处

五、编译与底层:
1、一个C++源文件从文本到可执行文件经历的过程:

对于C++源文件,从文本到可执行文件一般需要四个过程:
   1) 预处理阶段: 对源码代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件;
   2) 编译阶段: 将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件;
   3) 汇编阶段: 将编译阶段生成的汇编文件转换为机器码,生成可重定位目标文件;
   4) 链接阶段: 将多个目标文件及所需要的库链接成最终的可执行目标文件;

2、说一说C++的内存管理是怎样的:

在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。
   1) 代码段: 包括只读存储区和文本区,其中只读存储区存储字符串常量、文本区存储程序的机器代码。
   2) 数据段: 存储程序中已初始化的全局变量和静态变量
   3) bss段: 存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。
   4) 堆区: 调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
   5) 映射区: 存储动态链接库以及调用mmap函数进行的文件映射
   6) 栈: 使用栈空间存储函数的返回地址、参数、局部变量、返回值

3、说一说什么是内存泄漏(memory leak):

  内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费
具体分类:
1)堆内存泄漏(Heap leak): 对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
2)系统资源泄漏(Resource leak): 主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
2). 没有将基类的析构函数定义为虚函数: 当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

4、设计一下如何采用单线程的方式处理高并发:

   在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件。

5、内存泄漏检查工具:

使用varglind,mtrace检测。

6、说说select,epoll的区别,原理,性能,限制:
7、说一说C++ STL 的内存优化:
六、C++11:
1、C++11有哪些新特性:

1)auto关键字: 编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导。
2)nullptr关键字: nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
3)智能指针: C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
**4)初始化列表:**使用初始化列表来对类进行初始化。
5)右值引用: 基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
6)atomic原子操作: 用于多线程资源互斥操作
7)其他: 新增STL容器array以及tuple、可变参数模板、lambda。

2、请你详细介绍一下C++11中的可变参数模板、右值引用和lambda这几个新特性:

1)lambda: Lamda表达式是C++11中引入的一项新技术,利用Lamda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使得代码更可读。是一种匿名函数,即没有函数名的函数;Lamda函数的语法定义如下:
在这里插入图片描述
[captrue]::总是作为lambda的开始处,即开始标志。编译器可以根据该“标志”来判断出该函数是否为lambda函数。同时“捕捉列表”能够捕捉上下文中的变量以作为lambda函数使用。主要以下几种形式:
  — []: 默认不捕捉任何变量;
  — [var]: 表示值传递方式捕捉变量var;
  — [=]: 表示值传递方式捕捉所有作用域的变量(包括this);
  — [&var]: 表示引用传递捕捉变量var;
  — [&]: 表示引用传递捕捉所有外部变量(父作用域)
  — [var,var,…,var]: 默认以值得形式捕获指定的多个外部变量(逗号分隔),如果引用,需要显示&说明;
  — [=,&var]: 变量var以引用形式捕获,其余变量以传值形式捕获
  — [var,&]: 变量var以值的形式捕获,其余变量以引用形式捕获
  — [this]: 表示引用捕获当前对象(其实是复制指针);
  — [*this]: 表示通过值方式捕获当前对象;
(parameters): 参数列表。和C/C++中的普通函数参数一致,写法也一致。该部分是可选的,意味着如果我们不需要进行参数传递时,可以连同括号“()”一起省略掉。
mutable: 该关键字为一个修饰符。在默认的情况下,lambda函数总是返回一个const,而当我们在参数列表后面注明了“mutable”关键字之后,则可以取消其常量性质。若在lambda中使用了mutable修饰符,则“参数列表”是不可省略掉的(即使是参数为空)。
->return-type: 函数的返回值类型。和C/C++中的普通函数返回值类型的性质一样。主要目的是用来追踪lambda函数(有返回值情况下)的返回类型。若lambda函数不需要返回值,则可以直接将这部分省略掉
{statement}: 函数体。在该函数体中,除了可以使用参数列表中的变量外,还可以使用所有捕获到的变量(即[capture] 中的变量)。

#include <iostream>
#include <string>
using namespace std;
int main(int argc, char** argv) {

	int a = 1, b = 2, c = 3;
	// 引用捕捉:可以改变变量的值。对于值捕捉的c,有了mutable关键字,在内部c就不是常量,可以在里面改变却不影响外面
	auto retVal = [=, &a, &b]() mutable->int {

		cout << "before inner  c = "<<c << endl;
		a = 10;
		b = 20;
		c = 30;
		cout << "after inner  c = " << c << endl;
		return a + b;
	};
	cout << "sum = " << retVal()<< endl;
	cout << "a = " << a << " b = " << b << " c = " << c << endl;
	return 0;
}

在这里插入图片描述
2)可变参数模板: C++11新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。相应地,存在函数参数包,意味着这个函数参数可以接收任意数量的参数。
使用规则:

// 可变类模板
template<typename ... T> //T 模板参数包
class Tuple {};
Tuple<>t0;
Tuple<int> t1;
Tuple<int, char> t2;
// 可变参数函数模板
template<typename ... T> // 模板参数包
void f(T...args) // 函数参数包
f();
f(1);
f(3.4,"hello");

对于类模板来说,可变模板参数必须是模板参数列表中的最后一个参数,对于函数模板没有这个限制。如下代码:

template<typename ... T, typename U>
class Invalid{}; // 非法,永远无法推出U的类型,不满足可变模板参数是模板参数列表的最后一个

// 函数
template<typename ... T, typename U>
void valid(U u, T ... args); // 合法,可以推出U的类型
void inval(T ... args, U u); // 非法 ,无法推出U的类型
valid("1",1,2,3); // 此时U的类型是const char*, T是{int,int,int}

参数展开:

// 模板参数包
template<typename...T> 
void fun(T ... args) {
	cout << sizeof...(args) << endl; // 打印参数包中的参数的个数
}

展开参数包的函数必须要有两个,一个是递归函数、一个是终止递归函数。参数包args…在展开的过程中递归调用自己,每次调用参数包就会少一个,知道所有的参数都展开为止,当没有参数时,则调用非模板函数终止递归过程。如:

#include <iostream>
using namespace std;

// 最终递归函数
void print()
{
	cout << "empty" << endl;
}

// 展开函数
template <typename T, typename... Args>
void print(T head, Args... args)
{
	cout << head << ",";
	print(args...);
}

int main()
{
	print(1, 2, 3, 4);
	return 0;
}

递归:print(1,2,3,4)->print(2,3,4)->print(3,4)->print(4)->print();终止函数可以进行以下优化,减少总的递归次数。

#include <iostream>
//最终递归函数
 // 终止函数是这个的话,调用顺序:print(1,2,3,4)->print(2,3,4)->print(3,4)->print(4)
template <class T>
void print(T t) { cout << t << endl; } // #1 
// 终止函数是这个的话,调用顺序:print(1,2,3,4)->print(2,3,4)->print(3,4)
template <class T>
void print(T t, T t1) { cout << t << "****" << t1 << endl; } // #2
// 终止函数是这个的话,调用顺序:print(1,2,3,4)->print(2,3,4)
template <class T>
void print(T t, T t1,T t2) { cout << t << "---" << t1 << "-----" <<t2<< endl; }// #3 
/*
  总结:终止递归函数在递归过程中自动匹配。当定义多个终止函数时,优先匹配递归调用次数少的如#3,或者直观来讲选择参数多的。
*/

// 展开函数
template <typename T, typename... Args>
void print(T head, Args... args)
{
	cout << head << ",";
	print(args...);
}

int main()
{
	print(1, 2, 3, 4,6);
	return 0;
}

3):右值引用
这部分暂时省略。

六、操作系统:
1、说一下进程与线程的概念,以及为什么要有进程线程,其中有什么区别,他们各自又是怎么同步的:

进程: 进程是对程序运行时的封装,是系统进行资源调度和分配的基本单位,实现了操作系统的并发
线程: 线程是进程的子任务,是CPU调度和分配的基本单位,是实现进程内部的并发;线程是操作系统可识别的最小执行单位和调度单位。每个线程独占一个虚拟处理器:独自的寄存器组、指令计数器和处理器。
两种区别:
   1)进程在执行过程中
拥有独立的内存单元,而多个线程共享父进程的内存
。同一进程中的多个线程共享代码段(代码和常量)、数据段(全局和静态变量)、扩展段(堆存储)。但是每个线程拥有自己独立的栈,用来存放局部变量和临时变量
  2) 进程是资源分配的最小单位,线程是CPU调度的最小单位;
  3) 系统开销: 由于在创建或撤消进程时,系统都要为之分配或回收资源,如内存空间、I/o设备等。因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。类似地,在进行进程切换时,涉及到整个当前进程CPU环境的保存以及新被调度运行的进程的CPU环境的设置。而线程切换只须保存和设置少量寄存器的内容,并不涉及存储器管理方面的操作。可见,进程切换的开销也远大于线程切换的开销
  4) 通信:由于同一进程中的多个线程具有相同的地址空间,致使它们之间的同步和通信的实现,也变得比较容易。进程间通信IPC,线程间可以直接读写进程数据段(如全局变量)来进行通信——需要进程同步和互斥手段的辅助,以保证数据的一致性。在有的系统中,线程的切换、同步和通信都无须操作系统内核的干预。进程:管道、系统IPC(消息队列、信号了、信号、共享内存)以及套接字等。
  5).进程编程调试简单可靠性高,但是创建销毁开销大;线程正相反,开销小,切换速度快,但是编程调试相对复杂。
  6)进程间不会相互影响 ;线程一个线程挂掉将导致整个进程挂掉
  7)进程适应于多核、多机分布;线程适用于多核

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值