1.C和C++
a)C是面向过程的语言,是一个结构化的语言,考虑如何通过一个过程对输入进行处理得到输出;C++是面向对象的语言,更加抽象,主要特征是“封装、继承和多态”。封装隐藏了实现细节,使得代码模块化;派生类可以继承父类的数据和方法,扩展了已经存在的模块,实现了代码重用;多态则是“一个接口,多种实现”,通过派生类重写父类的虚函数,实现了接口的重用。
b)C和C++动态管理内存的方法不一,C用malloc/free,C++除此之外还有new/delete
c)C++支持函数重载,C不支持函数重载
d)C++中有引用,C中不存在引用的概念
e)C++ 更加严格和安全,如 const 正确性、指针和枚举类型的自动转换
C++ 版本 |
版本号 |
描述 |
C++98 |
1.0 |
第一个 C++ 标准 |
C++03 |
1.0.1 |
修正98标准中的一些bug |
C++11 |
2.0 |
全新的现代 C++ 标准 |
C++14 |
2.1 |
上一版本的小改进 |
C++17 |
2.5 |
改进版本 |
C++20 |
3.0 |
大版本,加入较多新特性 |
1) C++编译与执行的四阶段
a)预处理:根据文件中的预处理指令来修改源文件的内容, 产生.ii文件
b)编译:编译成汇编代码,产生汇编文件(.s文件)
c)汇编:把汇编代码翻译成目标机器指令,产生目标文件(.o或.obj文件)
d)链接:链接目标代码生成可执行程序,产生可执行文件(.out或.exe文件)
为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
注:
禁止在extern "C"中包含头文件,有可能会导致 extern "C" 嵌套,部分编译器对 extern "C" 嵌套层次有限制,嵌套层次太多会编译错误。
在C,C++混合编程的情况下,在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改
a)对象的静态类型和动态类型
静态类型:对象在声明时采用的类型,在编译时确定
动态类型:当前对象所指的类型,在运行期决定,对象的动态类型可变,静态类型无法更改
b)静态绑定和动态绑定
静态绑定:绑定的是对象的静态类型,函数依赖于对象的静态类型,在编译期确定
动态绑定:绑定的是对象的动态类型,函数依赖于对象的动态类型,在运行期确定
虚函数使用动态绑定,其他的静态绑定
空类成员函数, 只有当实际使用这些函数的时候,编译器才会去定义它们
· 默认构造函数
· 缺省析构函数
· 缺省拷贝构造函数
· 缺省拷贝赋值运算符
· 缺省取址运算符
· 缺省const取址运算符
构造,拷贝,移动和析构函数提供了对象的生命周期管理方法:
构造函数(constructor): X()
拷贝构造函数(copy constructor): X(const X&)
拷贝赋值操作符(copy assignment): operator=(const X&)
移动构造函数(move constructor): X(X&&)
移动赋值操作(move assignment): operator=(X&&) // C++11
析构函数(destructor): ~X() // C++11
注:
尽量使用C++的语言特性来编程,如使用string而不是 char* , 使 用vector而不是原生数组,使用namespace而不是static。
2. 继承:私有、保护与公有
class RichMan
{
public:
RichMan();
~RichMan();
int m_company;
private:
int m_money;
int m_car;
protected:
int m_house;
};
m_company,自己,友元,子类,外部都可访问
· m_money、m_car,自己,友元可访问,子类和外部不可访问
· m_house,自己,友元可访问,子类也可访问,外部不可访问
公有继承:public成员保持不变,private成员不可见,protected成员也保持不变;
私有继承:public和protected变成了private;纯粹是一种实现技术,在软件“设计”层面没有意义,只及于软件实现层面。
保护继承:public变成了protected,protected保持不变。
多重继承提供了一种更简单的组合来实现多种接口或者类的组装与复用。
可用于实现接口分离与多角色组合。
问题:
1. 菱形继承所带来的数据重复,及名字二义性。因此,引入virtual继承来解决这类问题;
2. 即便不是菱形继承,多个父类之间的名字也可能存在冲突,从而导致的二义性;
3. 如果子类需要扩展或改写多个父类的方法时,造成子类的职责不明,语义混乱;
4. 相对于委托,继承是一种白盒复用,即子类可以访问父类的protected成员, 会导致更强的耦合。由于耦合了多父类,相对于单根继承,这会产生更强的耦合关系。
3. 虚函数与纯虚函数
用于实现多态(polymorphism,编译器针对虚函数产生可在运行时确定被调用函数的代码)的机制,通过基类访问派生类定义的函数。虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
虚函数借助于指针或者引用来达到多态的效果,虚函数的标志是“virtual”关键字,基类声明的虚函数,在派生类中也是虚函数,即使不再用virtual关键字。
VTABLE实际是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但派生类的VTABLE与基类的有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写。
类的实例对象不包含虚函数表,只有虚指针;
一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。
设计一个基类的时候,如果发现一个函数需要在派生类里有不同的表现,那么它就应该是虚的。从设计的角度讲,出现在基类中的虚函数是接口,出现在派生类中的虚函数是接口的具体实现。通过这样的方法,就可以将对象的行为抽象化。
在普通的虚函数后面加上" =0"这样就声明了一个pure virtual function. 纯虚函数的意思是:一个抽象类,不要实例化,纯虚函数用来规范派生类的行为,实际上就是所谓的“接口”。它告诉使用者,派生类都会有这个函数。
1) 当在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
2) 避免一个类被实例化,且在编译时就被发现,那使用pure virtual funcion
3) 这个方法必须在派生类(derived class)中被实现;
析构函数也可以是虚的,甚至是纯虚的.
class A {
public:
virtual ~A()=0; // 纯虚析构函数
};
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的,否则派生类的析构函数用不上,会造成资源的泄漏。
而纯虚的析构函数并没什么作用,通常只有在希望将一个类变成抽象类(不能实例化的类),而这个类又没有合适的函数可被纯虚化的时候,可以使用纯虚的析构函数来达到目的。
如析构函数不被声明成虚函数,编译器实施静态绑定,在删除指向派生类的基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样会造成派生类对象析构不完全。
避免虚函数重载时,因参数声明不一致带来的困惑和问题,所有虚函数均不允许声明缺省参数值。
示例:虚函数display缺省参数值text是由编译时刻决定的,而非运行时刻,没有达到多态的目的:
virtual void Display(const std::string& text = "Sub!") {
std::cout << text << std::endl;
}
注:
构造函数不能是虚的
禁止在构造函数和析构函数中调用虚函数(只有基类构造完成后,才会完成派生类的构造,从而导致未实现多态的行为), 同样的道理也适用于析构函数
4. 深浅拷贝,拷贝构造
简单来说,如果一个类拥有资源,当这个类的对象发生复制过程时,如果资源重新分配就是深拷贝;反之没有重新分配资源,就是浅拷贝。
生成一个实例化的对象会调用一次普通构造函数,而用一个对象去实例化一个新的对象所调用的则是拷贝构造函数.
调用情形:
1)用类的一个对象去初始化另一个对象的时候
2)当函数参数是类的对象时,就是值传递的时候;如果是引用传递则不会调用
3)当函数的返回值是类的对象或者引用的时候;
代码示例:
#include
#include
using namespace std;
class A{
public:
A(int i){ data = i;} //自定义构造
A(A && a); //拷贝构造
int getdata(){return data;}
private:
int data;
};
A::A(A && a){ //拷贝构造函数
data = a.data;
cout <<"拷贝构造函数执行完毕"<
拷贝构造函数和赋值运算符重载有以下不同:
1) 拷贝构造函数生成新的类对象,而赋值运算符不能
2) 由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象是否和新建对象相同。而赋值运算符则需要这个操作,另外赋值运算中如果原来的对象中有内存分配要先把内存释放掉
注:当类中有指针类型的成员变量时,一定要重写拷贝构造函数和赋值运算符,不要使用默认的。
5. 单参构造,explicit