目录
1. 面向对象
面向对象的基本概念:类、对象、继承。
基本特征:封装、继承、多态。
- 封装:将低层次的元素组合起来形成新的、更高实体的技术;
- 继承:通过类派生机制来支持继承。被继承的类型称为基类或超类,新产生的类为派生类或子类。
- 多态:“一个接口,多种方法”。
- 静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
- 函数重载
- 函数模板的使用 :函数模板是通用的函数描述,也就是说,使用泛型来定义函数。
// 使用函数模板,将类型作为参数传递 template<class T> class Swa(T a,T b) { T temp; temp = a; a = b; b = temp; };
- 动态多态: 即运行时的多态,在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。
- 静态多态:也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。
类
- 类的默认成员函数:默认构造函数、析构函数、复制构造函数、赋值操作符重载函数,取地址操作符重载函数,const修饰的取地址操作符重载函数。
- static静态成员变量可以在一个类的实例之间共享。
- 多态:将基类类型的指针或者引用指向派生类型的对象。多态通过虚函数机制实现。多态的作用是接口重用。
- 重载和覆盖(重写)
- 虚函数是基类希望派生类重新定义的函数,派生类重新定义基类虚函数的做法叫做覆盖;
- 重载就在允许在相同作用域中存在多个同名的函数,这些函数的参数表不同。重载的概念不属于面向对象编程,编译器根据函数不同的形参表对同名函数的名称做修饰,然后这些同名函数就成了不同的函数。
- 重载的确定是在编译时确定,是静态的;虚函数则是在运行时动态确定。
- 类的初始化列表
- 类的构造函数参数后跟初始化列表。在构造函数执行时,先执行初始化列表,实现变量的初始化,然后再执行函数内部的语句。
class Data { public: Data():_b(12),_a(_b) { } private: int _a; int _b; };
- 成员变量的初始化顺序与在类中的声明顺序有关,与初始化列表的顺序无关。
- 类的构造函数参数后跟初始化列表。在构造函数执行时,先执行初始化列表,实现变量的初始化,然后再执行函数内部的语句。
- 共有成员,受保护成员,私有成员
- public:类内、类的对象;派生类内、派生类对象—>>均可访问。
- protected:类内、派生类内—>>可以访问;类的对象、派生类的对象–>>不可访问。
- private:只有类内部–>>可以访问;类的对象、派生类、派生类的对象,统统的–>>不可访问。
- 公有继承、受保护继承、私有继承
- 公有继承时,公有继承时,对基类的公有成员和保护成员的访问属性不变,派生类的新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员。派生类的对象只能访问派生类的公有成员(包括继承的公有成员),访问不了保护成员和私有成员。
- 受保护继承时,基类的公有成员和保护成员被派生类继承后变成保护成员,派生类的新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员。派生类的对象不能访问派生类继承基类的公有成员,保护成员和私有成员。
- 私有继承时,基类的公有成员和保护成员都被派生类继承下来之后变成私有成员,派生类的新增成员可以访问基类的公有成员和保护成员,但是访问不了基类的私有成员。派生类的对象不能访问派生类继承基类的公有成员,保护成员和私有成员。
- const成员和引用成员只能用构造函数初始化列表而不能用赋值初始化。
- 将类定义为抽象基类或者将构造函数声明为private可以阻止一个类被实例化。将构造函数声明为private意思是不允许类外部创建类对象,只能在类内部创建对象。
- 子类调用析构函数过程:析构函数调用的次序是先派生类的析构后基类的析构,也就是说在基类的的析构调用的时候,派生类的信息已经全部销毁了。定义一个对象时先调用基类的构造函数、然后调用派生类的构造函数;析构的时候恰好相反:先调用派生类的析构函数、然后调用基类的析构函数。
- 多重继承和虚继承(http://c.biancheng.net/view/2280.html)
- 多重继承:派生类继承多个基类,派生类为每个基类(显式或隐式地)指定了访问级别。
- 虚继承:在多重继承下,一个基类可以在派生层次中出现多次。虚继承是一种机制,类通过虚继承指出它希望共享其虚基类的状态。在虚继承下,对给定虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。共享的基类子对象称为虚基类。
- 继承的内存分布
- 拷贝构造函数为什么传引用:若为值传递,这样从形参到实参的转换会调用拷贝构造函数。参数为引用可防止拷贝构造函数的无限递归,最终导致栈溢出。这也是编译器的强制要求。
- 抽象类:抽象类是特殊的类,只是不能被实例化(将定义了一个或多个纯虚函数的类称为抽象类)
- 接口:C++种没有interface关键字,可以使用抽象类来定义接口。接口不包含方法的实现,一般来说接口是对动作的一个定义。派生类必须实现接口未实现的方法,一个类可以直接继承多个接口。
- 抽象类与接口的相同点:不能实例化;包含未实现的方法声明。
- 抽象类与接口的区别:
- 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为)进行抽象。
- 当想约束多个实现类具有统一的行为,但是不在乎每个实现类如何具体实现,可以使用接口。
- 拷贝构造函数调用时机:
- 当用类的一个对象去初始化该类的另一个对象时
- 如果函数的形参是类的对象,调用函数,进行形参和实参结合时
- 如果函数的返回值是类的对象,函数执行完成返回调用者时
- 如何避免调用拷贝构造函数?
- 不采用值拷贝(尽量用引用拷贝)
- 禁用拷贝构造含数据,方法是将构造函数私有化
- 将拷贝构造函数声明为explicit,则会阻止隐式拷贝构造函数的调用,只能显示调用。
- 友元函数(friend):类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。
- 尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
- 友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
- 可以直接调用友元函数,不需要通过对象或指针
- 因为友元函数没有this指针,则参数要有三种情况:
- 要访问非static成员时,需要对象做参数;
- 要访问static成员或全局变量时,则不需要对象做参数;
- 如果做参数的对象是全局对象,则不需要对象做参数
- 类模板和模板类
- 一个类模板(类生成类)允许用户为类定义一种模式,使得类中的某些数据成员、默认成员函数的参数,某些成员函数的返回值,能够取任意类型(包括系统预定义的和用户自定义的)。
- 模板的整个声明与实现,其实完全都是一个声明,因为没有确定的数据类型,所以在实例之前,编译器无法知道该模板应以什么样的方式去编译。
template <typename T> class Complex{ public: //构造函数 Complex(T a, T b) { this->a = a; this->b = b; } //运算符重载 Complex<T> operator+(Complex &c) { Complex<T> tmp(this->a+c.a, this->b+c.b); return tmp; } private: T a; T b; } int main() { //对象的定义,必须声明模板类型,因为要分配内容 Complex<int> a(10,20); Complex<int> b(20,30); Complex<int> c = a + b; return 0; }
- 模板类:类模板实例化后的一个产物,比如STL中的vector,list,map等都是常见的模板类。
- 定义一个字符串(char*)的赋值运算符函数
CMyString& CMyString::operator =(const CMyString& str) { if(this == &str)return *this; delete []m_pData; m_pData = nullptr; m_pData = new char[strlen(str.m_pData) + 1]; strcpy(m_pData, str.m_pData); return *this; }
- 要把返回值的类型声明为该类型的引用,并在函数结束前返回实例自身的引用(*this)。只有返回一个引用,才可以允许连续赋值。
- 要将传入参数的类型声明为常量引用。如果传入的参数不是引用而是实例,那么从形参到实参会调用一次复制构造函数,产生一定的消耗。
- 要释放实例自身已有的内存。如果忘记在分配新内存之前释放自身已有的空间,会出现内存泄漏。
- 判断传入的参数和当前实例是不是同一个实例。如果不判断而且两者是同一个实例时,释放了自身的内存则传入参数的内存也被释放,找不到需要赋值的内容了。
- 复制构造函数和赋值操作符重载函数的使用时机
- 复制构造函数被用来“以同型对象初始化自身对象”,赋值操作符重载函数被用来“从另一个同型对象中拷贝其值到自我对象”。
- 当有一个新对象被定义,一定会有一个构造函数被调用,不可能调用赋值操作。
// 都调用复制构造函数 Widget w2(w1); Widget w3 = w1;
- 如果没有新对象被定义,就不会有构造函数被调用,那么调用赋值操作符重载函数。
Widget w1; // 调用默认构造函数 w1 = w2; // 调用赋值操作符重载函数
虚函数
-
虚函数的实现:虚函数主要是通过虚函数表(V-Table)来实现的。
- 如果一个类中包含虚函数(virtual修饰的函数),那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。
- 这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。
- 虚函数表在编译器建立;而对象的隐藏成员(虚函数指针)在运行期构造函数被调用时建立的。
- 当一个派生类继承了两个含有虚函数的基类时,这个派生类有两个虚函数表,它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。
-
虚函数和纯虚函数的区别
两者都实现“运行时多态”,具体区别如下:- 虚函数:父类中提供虚函数的实现,为子类提供默认的函数实现。子类可以重写父类的虚函数实现子类的特殊化。
- 包含纯虚函数的类,被称为是“抽象类”。抽象类不能new出对象,只有实现了这个纯虚函数的子类才能new出对象。纯虚函数必须在派生类中被实现。纯虚函数更像是“只提供申明,没有实现”,是对子类的约束,是“接口继承”。
// 虚函数 virtual void f(){ cout << "virtual" << endl; } // 纯虚函数 virtual void f() = 0;
-
继承中,基类析构函数为什么是虚函数?
编译器总是根据类型来调用类成员函数。但是一个派生类的指针可以安全地转化为一个基类的指针。这样删除一个基类的指针的时候,C++不管这个指针指向一个基类对象还是一个派生类的对象,调用的都是基类的析构函数而不是派生类的。如果你依赖于派生类的析构函数的代码来释放资源,而没有重载析构函数,那么会有资源泄漏。 -
为什么构造函数不能是虚函数?
虚函数采用一种虚调用的方法。虚调用是一种可以在只有部分信息的情况下工作的机制。如果创建一个对象,则需要知道对象的准确类型,因此构造函数不能为虚函数。 -
只要基类在定义成员函数时已经声明了 virtual关键字,在派生类实现的时候覆盖该函数时,virtue关键字可加可不加,不影响多态的实现。子类的空间里有父类的所有变量(static除外)。
-
虚函数与虚函数表的具体原理:见博客
2. 类型和变量
C++强制类型转换
- const_cast:用于强制去掉const这种不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。常量指针、引用或对象被转换为非常量,且仍指向原处。
- static_cast:用于数据类型的强制转换,强制将一种数据类型转换为另一种数据类型。没有动态检查,不安全。
- 用于类层次结构中基类和派生类之间指针或引用的转换。进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类的指针或引用转换为派生类表示)由于没有动态类型检查,是不安全的。
- 用于基本数据类型之间的转换,如把int转换成char。这种转换的安全也要开发人员来保证。
- 把空指针转换成目标类型的空指针。
- 把任何类型的表达式转换为void类型。
- dynamic_cast:运行时处理的,运行时要进行类型检查。可在类层次间进行上行转换时,比static_cast安全。
- reinterpret_cast:是强制类型转换符用来处理无关类型转换的,通常为操作数的位模式提供较低层次的重新解释。可改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。转换过程仅仅只是比特位的拷贝。
- RTTI(Run Time Type Identification) 运行时类型识别
程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。RTTI机制的底层简单来讲就是在一个类的虚函数表里面添加了一个新的条目。RTTI提供了两个非常有用的操作符:typeid和dynamic_cast。- typeid操作符,返回指针和引用所指的实际类型;
- 当类中不存在虚函数时,typeid是编译时期的事情,也就是静态类型
- 当类中存在虚函数时,typeid是运行时期的事情,也就是动态类型
- dynamic_cast操作符,将基类类型的指针或引用安全地转换为其派生类类型的指针或引用。
- typeid操作符,返回指针和引用所指的实际类型;
- auto类型:auto 并不能代表一个实际的类型声明,只是一个类型声明的“占位符”。使用 auto 声明的变量必须马上初始化,以让编译器推断出它的实际类型,并在编译时将 auto 占位符替换为真正的类型。
- 当不声明为指针或引用时,auto 的推导结果和初始化表达式抛弃引用和 cv(const和 volatile) 限定符后类型一致。
- 当声明为指针或引用时,auto 的推导结果将保持初始化表达式的 cv 属性。
const static volatile mutable
- const用来定义常量,修饰函数的返回值和形参,还可以修饰函数的定义体,定义类的const成员函数。被const修饰的东西受到强制保护,可以预防意外的变动,提高了程序的健壮性。
- const和#define的区别
- const和#define都可以定义常量,但是const用途更广。
- const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
- 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
- static的特点:
- 静态全局变量只初始化一次,防止在其他文件单元中被引用。
- 静态数据成员在全局数据区分配空间(不在栈中)。
- 静态函数只能在声明它的文件中可见,不能被其他文件使用。
- 静态成员变量可以在一个类的实例之间共享。
- 静态成员函数没有this指针的开销,只能访问静态的函数和变量。
- const 不同位置的区别:如果const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。
- const char * :定义一个指向字符常量的指针,不能用ptr来修改所指向的内容,并不意味着指向的值不能修改。
- char const *:与上述相同。
- char * const :定义一个指向字符的指针常数,即常指针,地址不能变,但可以修改指向的内容。
- static关键字的作用:static总是使得变量或对象的存储形式变成静态存储,连接方式变成内部连接,对于局部变量(已经是内部连接了),它仅改变其存储方式;对于全局变量(已经是静态存储了),它仅改变其连接类型。
- static数据成员和static成员函数
- static数据成员独立于类的任意对象而存在;每个static数据成员是与类关联的对象,并不与该类的对象相关联。static数据成员(const static数据成员除外)必须在类定义体的外部定义。不像普通数据成员,static成员不是通过类的构造函数进行初始化,而是应该在定义时进行初始化。
- static成员函数没有this形参,它可以直接访问所属类的static成员,不能直接使用非static成员。因为static成员不是任何对象的组成部分,所以static成员不能被声明为const。同时,static成员函数也不能被声明为虚函数。
- 类使用static成员
- static 成员的名字是在类的作用域中,可以避免与其他类的成员或全局对象名字冲突。
- 可以实施封装。static 成员可以是私有成员,而全局对象不可以。
- static 成员是与特定类关联的,可清晰地显示程序员的意图。
- static 数据成员必须在类定义体的外部定义,static 关键字只能用于类定义体内部的声明中,定义不能标示为static。 不像普通数据成员,static成员不是通过类构造函数进行初始化,也不能在类的声明中初始化,而是应该在定义时进行初始化。保证对象正好定义一次的最好办法,就是将static 数据成员的定义放在包含类非内联成员函数定义的文件中。
- static成员变量定义放在cpp文件中,不能放在初始化列表中。
- 不可同时用const和static修饰成员函数:
- 在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
- 两者的语意是矛盾的。static的作用是表示该函数只作用在类型的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类型的静态变量没有关系。
- static函数:与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件。static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝。
- volatile:提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
- mutable:当一个成员变量被声明为mutable时,它显示地表明"这个变量可能在一个const语义环境中被更改”。即mutable 用来修饰类的数据成员,被 mutable 修饰的数据成员,可以在 const 成员函数中修改。
变量和运算符
- 全局变量和局部变量
- 生命周期不同:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
- 内存分配不同,全局变量分配在全局数据段并且在程序开始运行的时候被加载。局部变量则分配在堆栈里面。操作系统和编译器就是根据两者的位置不同来区分的。
- 使用方式不同:通过声明后全局变量程序的各个部分都可以用到,局部变量只能在局部使用。
- 前置和后置运算符:
- 前置运算符(++等)的实现比较高效,自增之后,将*this指针直接返回即可,一定要返回this指针。
- 后置运算符(++等)的实现比较麻烦,因为要返回自增之前的对象,所以先将对象进行拷贝一份,再进行自增,最后返回那个拷贝。
指针和引用
- 引用及声明时要注意的问题:
- 引用就是某个目标变量的“别名”,对引用的操作与对变量直接操作效果完全相同。
- 申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。
- 声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。
- 引用作为函数参数:
- 传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
- 使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。
- 使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。
- 引用作为函数返回值
- 优点:在内存中不产生被返回值的副本。
- 不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
- 不能返回函数内部new分配的内存的引用。虽然不存在局部变量的被动销毁问题,但有其他问题。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。
- 可以返回类成员的引用,但最好是const。主要原因是当对象的属性是与某种业务规则相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
- 常引用:如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
cpp 常引用声明方式:const 类型标识符 &引用名=目标变量名;
- 左值与右值:
- 左值,是指向一个指定内存的东西。左值可看作是一个关联了名称的内存位置,允许程序的其他部分来访问它。
- 右值,是一个临时值,不能被程序的其他部分访问。虽然没有名称,但可以从程序的其他部分访问到。
- 程序中square(5)是右值,它代表了一个由编译器创建的临时内存位置,以保存由函数返回的值。该内存位置仅被访问一次,也就是在main函数中赋值语句的右侧。在此之后,它就会立即被删除,再也不能被访问了。
- 当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址。
// 左值 int x = 34; int &lRef = x; // 右值 int square(int a) { return a * a; } int main() { int x = 0; x = square(5); cout << x << endl; return 0; } // 函数返回了一个右值(6),不能被赋值 int setValue() { return 6; } // ... somewhere in main() ... setValue() = 3; // error! // 函数返回了一个左值,可以被赋值 int global = 100; int& setGlobal() { return global; } // ... somewhere in main() ... setGlobal() = 400; // OK
- 右值引用:
- 右值引用表示一个本应没有名称的临时对象,使用"&&"来定义,右值引用就是必须绑定到右值的引用。
- 声明一个右值引用,给一个临时内存位置分配一个名称,这使得程序的其他部分访问该内存位置成为了可能,并且可以将这个临时位置变成一个左值。因此右值引用本质是“左值”。
- 不能将一个右值引用直接绑定到一个左值上。
- 左值引用和右值引用的应用:
- 返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。可以将一个左值引用绑定到这类表达式的结果上。
- 返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。不能将一个左值引用绑定到这类表达式上,但可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
- move构造:可以通过调用一个名为move的新标准库函数来获得绑定到左值上的右值引用。
- 虽然不能将一个右值引用直接绑定到一个左值上,但可以显示地将一个左值转换为对应的右值引用类型。
- move调用告诉编译器有一个左值,但希望像一个右值一样处理它。在调用move之后,不能对移后源对象的值做任何假设。可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。
- move构造可以减少不必要的临时对象的创建、拷贝以及销毁,解决了在用临时对象或函数返回值给左值对象赋值的深拷贝。
// 不能将一个右值引用绑定到一个右值引用类型的变量上 int&& rr1 = 42; // 正确:字面常量是右值 //int&& rr2 = rr1; // 错误:表达式rr1是左值 //int rr = &&rr1; // 不能将一个右值引用直接绑定到一个左值上 int i = 5; int&& rr3 = std::move(i); // ok
- move语义的理解
- std::move是将对象的状态或所有权从一个对象转移到另一个对象,没有内存的搬运或拷贝。
- move构造其实和浅拷贝比较相似,区别是move构造将源对象清空了。(指针置为nullptr,容器就clear所有元素)
- copy就是照着别人的东西复制一份,move是将别人的东西占为己有,即把子集的指针指过去,原属主的指针指向别处。
- 右值引用的好处:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率;
- 能够更简洁明确地定义泛型函数
- 指针和引用的区别:指针和引用都提供了间接操作对象的功能。
- 指针定义时可以不初始化,而引用在定义时就要初始化,和一个对象绑定,而且一经绑定,只要引用存在,就会一直保持和该对象的绑定;
- 赋值行为的差异:指针赋值是将指针重新指向另外一个对象,而引用赋值则是修改对象本身;
- 指针之间存在类型转换,而引用分const引用和非const引用,非const引用只能和同类型的对象绑定,const引用可以绑定到不同但相关类型的对象或者右值。
- 数组和指针的区别:
- 数组要么在全局数据区被创建,要么在栈上被创建;指针可以随时指向任意类型的内存块;
- 修改内容上的差别:
char a[] = “hello”; a[0] = ‘X’; char *p = “world”; // 注意p 指向常量字符串 p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
- 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof 指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。C++/C 没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
- 空指针和悬垂指针
- 空指针是指被赋值为NULL的指针;悬垂指针指向曾经存在的对象,但该对象已经不再存在。
- 空指针可以被多次delete,而悬垂指针再次删除时程序会变得非常不稳定。
- 使用空指针和悬垂指针都是非法的,而且有可能造成程序崩溃,如果指针是空指针,尽管同样是崩溃,但和悬垂指针相比是一种可预料的崩溃。
- 智能指针
- 当类中有指针成员时,一般有两种方式来管理指针成员:一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;另一种更优雅的方式是使用智能指针,从而实现指针指向的对象的共享。
- 智能指针的一种通用实现技术是使用引用计数。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。
- 每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。
- 三种智能指针:
- unique_ptr:唯一拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。
- 相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放
- unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。
- unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
- shared_ptr:基于引用计数模型。每次有 shared_ptr 对象指向资源,引用计数器就加1;当有 shared_ptr 对象析构时,计数器减1;当计数器值为0时,被指向的资源将会被释放掉。
- 该类型的指针可复制和可赋值,即其可用于STL容器中。此外,shared_ptr 指针可与多态类型和不完全类型一起使用。
- 主要缺点:无法检测出循环引用(后面会细说),如一颗树,其中既有指向孩子结点的指针又有指向父亲结点的指针,即孩子父亲相互引用。这会造成资源无法释放,从而导致内存泄露。为了 fix 这个问题,引入了另一个智能指针:weak_ptr。
- weak_ptr:指向有shared_ptr 指向的资源(即其需要shared_ptr的参与,其辅助 shared_ptr 之用),但是不会导致计数。一旦计数器为0,不管此时指向资源的 weak_ptr 指针有多少,资源都会被释放,而所有的这些 weak_ptr 指针会被标记为无效状态,即 weak_ptr作为观察shared_ptr 的角色存在着,shared_ptr 不会感受到 weak_ptr 的存在。(使用weak来解决循环引用不能释放内存的问题的例子)
- unique_ptr:唯一拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。
- 哑指针:只是一个指向,除此以外不会有其他任何动作,所有的细节必须程序员来处理,比如指针初始化,释放等等
- 野指针:指向被释放的内存或者访问受限的指针 。造成的原因:指针未被初始化;被释放的指针没有被置为NULL;指针越界操作。
- void*:无类型指针。
- void的作用:(注释"和限制程序)
- 当函数不需要返回值值时,必须使用void限定,这就是我们所说的第一种情况。例如:void func(int a,char *b)
- 当函数不允许接受参数时,必须使用void限定,这就是我们所说的第二种情况。例如:int func(void)。
- 使用规则:
- void 指针可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。
- 在 ANSI C 标准中,不允许对 void 指针进行一些算术运算如 p++ 或 p+=1 等,因为既然 void 是无类型,那么每次算术运算我们就不知道该操作几个字节
- void 几乎只有"注释"和限制程序的作用,不能定义一个 void 变量。
- void *不可无需强制类型转换地赋给其它类型的指针。因为"无类型"可以包容"有类型",而"有类型"则不能包容"无类型"。
- 用void和voidconst 修饰参数,表示任何类型的指针都可以传入其中。
- void的作用:(注释"和限制程序)
- NULL和nullptr的区别
- NULL是宏,定义为#define NULL ((void *)0),但是C++是强类型语言, v o i d ∗ void* void∗不能隐式转换成其他类型的指针,因此C++中NULL实际上是0。
- 为了解决NULL代指空指针存在的二义性,引入了nullptr,因此代表空指针应使用nullptr,NULL当0用。
- 用NULL代替0表示空指针在函数重载时会有问题,以下函数中传NULL为参数,会转换为int,传nullptr为参数,转换为void*。
void func(void* t) { cout << "func( void* )" << endl; } void func(int i) { cout << "func( int ) " << endl; }
其他
- 结构与联合的区别:
- 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
- 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。
- stuct中存在内存对齐,对于32位机器,是4字节对齐,64位机器是8字节对齐。
- 字节对齐
3. 内存、编译、运行
内存分配和计算
-
sizeof计算的是在栈中分配的内存大小。
- 不管是什么类型的指针,大小一定是4个字节。
- char占1个字节,int占4个字节,unsigned int占4个字节,short int占2个字节,long int占8个字节(32位系统4个字节),float占4个字节,double占8个字节,一个空类占1个字节,单一继承的空类占1个字节,虚继承的类占4个字节(涉及到虚指针)。
- 数组的长度:
若指定了数组长度,则不看元素个数,总字节数=数组长度*sizeof(元素类型)。
若没有指定长度,则按实际元素个数类确定。
若是字符数组,则应考虑末尾的空字符。 - 结构体对象的长度:在默认情况下,为方便对结构体内元素的访问和管理,当结构体内元素长度小于处理器位数的时候,便以结构体内最长的数据元素的长度为对齐单位,即为其整数倍。若结构体内元素长度大于处理器位数则以处理器位数为单位对齐。
- 对函数使用sizeof,在编译阶段会被函数的返回值的类型代替。
- sizeof后如果是类型名则必须加括号,如果是变量名可以不加括号,这是因为sizeof是运算符。
- 当使用结构类型或者变量时,sizeof返回实际的大小。当使用静态数组时返回数组的全部大小,sizeof不能返回动态数组或者外部数组的尺寸。
-
size of和strlen的区别:
- sizeof的返回值类型为size_t(unsigned int)
- sizeof是运算符,而strlen是函数
- sizeof可以用类型做参数,其参数可以是任意类型的或者是变量、函数,而strlen只能用char*做参数,且必须是以’\0’结尾
- 数组作sizeof的参数时不会退化为指针,而传递给strlen是就退化为指针
- sizeof是编译时的常量,而strlen要到运行时才会计算出来,且是字符串中字符的个数而不是内存大小
-
malloc/free和new/delete
- malloc/free是C/C++标准库函数,new/delete是C++运算符。他们都可以用于动态申请和释放内存。
- 对于内置类型数据而言,二者没有多大区别。malloc申请内存的时候要制定分配内存的字节数,而且不会做初始化;new申请的时候有默认的初始化,同时可以指定初始化;
- 对于类类型的对象而言,用malloc/free无法满足要求的。对象在创建的时候要自动执行构造函数,消亡之前要调用析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制之内,不能把执行构造函数和析构函数的任务强加给它,因此,C++还需要new/delete。
-
delete与 delete []区别:
- delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数。当delete操作符用于数组时,它为每个数组元素调用析构函数,然后调用operator delete来释放内存。
- 对于内建简单数据类型,delete和delete[]功能是相同的。
- 对于自定义的复杂数据类型,delete和delete[]不能互用。delete[]删除一个数组,delete删除一个指针。
- 用new分配的内存用delete删除;用new[]分配的内存用delete[]删除。
-
malloc的原理:malloc函数的实质体现在它有一个可以将可用内存块连接成一个长的列表的空闲链表,当调用链表时,它沿着连接表寻找一个大到可以满足用户请求所需内存,将内存一分为二,将分配给用户那块内存传给用户,剩下的那块返回连接表。
-
内存泄漏:动态申请的内存空间没有被正常释放,但也不能继续被使用的情况。解决内存泄漏可以使用智能指针。
-
编译程序的内存占用分为以下几个部分:
- 栈区(stack): 由编译器自动分配释放 ,存放函数参数值,局部变量值等。其操作方式类似于数据结构中的栈。栈是向低地址扩展的数据结构,大小很有限。
- 堆区(heap): 一般由程序员分配释放(new/malloc、delete/free),若程序员不释放,程序结束时可能由OS回收 。容易产生碎片,分配方式类似于链表。堆是向高地址扩展,是不连续的内存区域,空间相对大且灵活。
- 全局/静态区(static):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
- 文字常量区:常量字符串就是放在这里的。 程序结束后由系统释放空间。
- 程序代码区:存放函数体的二进制代码。
PS:堆栈统称为动态数据区。相比于栈,堆的空间大,访问慢。访问堆的具体单元组要两次访问内存,第一次取得指针,第二次取数据。而栈只需要一次。另外,堆的内容被操作系统交换到外存的概率大。
-
深拷贝与浅拷贝
- 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,浅拷贝只是指向被复制的内存地址,如果原来对象被修改,那么浅拷贝出来的对象也会被修改。默认的复制构造函数只是完成了对象之间的伪拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
- 深拷贝:在计算机中开辟一块新内存用于存放复制的对象。因此要用new或者malloc等。自定义复制构造函数需要注意,对象之间发生复制,资源重新分配,即A有5个空间,B也应该有5个空间,而不是指向A的5个空间。
-
类及对象的内存:
- 类不是一个实体,而是一种抽象的类型,所以不占用系统的存储空间,所以是不会容纳数据的。只有当建立对象之后,系统才会对其分配内存。每一个对象在建立时候,应该对其初始化。
- 定义一个空类,里面没有任何成员变量和成员函数,对该类型求sizeof,得到结果为1。空类的实例不包含任何信息,本来求sizeof应该是0,但当我们声明该类型的实例时,它必须在内存中占有一定的空间,否则无法使用这些实例。
- 若一个空类中只定义了构造函数和析构函数,再对该类型求sizeof,得到结果仍为1。调用析构函数和构造函数只需要知道函数的地址即可,而这些函数的地址只与类相关,而与类的实例无关,编译器不会因为这两个函数在实例内添加任何额外的信息。
编译
- 在C++ 程序中调用被C 编译器编译后的函数,加extern “C”的原因
- extern是C/C++语言中表明函数和全局变量作用范围的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。
- 通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。extern "C"是连接申明(linkage declaration),被extern "C"修饰的变量和函数是按照C语言方式编译和连接的。C++支持函数重载,而C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:void foo( int x, int y );该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。
- extern “C”这个声明的真实目的:解决名字匹配问题,实现C++与C的混合编程。
- 头文件中的ifndef/define/endif的作用:这是C++预编译头文件保护符,保证即使文件被多次包含,头文件也只定义一次。
- #include<file.h> 与 #include "file.h"的区别
前者是从标准库路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h。 - C++源文件从文本到可执行文件的过程:
- 预处理:对所有的define进行宏替换;处理所有的条件编译#idef等;处理#include指令;删除注释等。
- 编译:将预处理后的文件进行词法分析、语法分析、语义分析以及优化相应的汇编文件
- 汇编:将汇编文件转换成机器能执行的代码
- 链接:包括地址和空间分配,符号决议和重定位
- 静态库和动态库
- 静态库(lib):链接时将程序放进可执行的程序中;可产生多个副本;不依赖程序运行。编译时需要。
- 动态库(dll):程序运行时,加载时才会到动态库找函数;多线程共享;依赖程序运行。运行时需要。
运行
- 全局对象的构造函数会在main函数之前执行;可以用_onexit 注册一个函数,在main函数后执行。
- C++里面是不是所有的动作都是main()引起的?
不是,全局变量的初始化就不是,是靠全局变量的构造函数引起的。 - debug和release的区别
- debug:通常称为调试版本,包含着调试信息,便于程序员调试
- release:称为发布版本,它往往是经过各种优化,使得程序在代码大小和运行速度是最优的,方便用户使用。
- main函数的返回值:用于说明程序的退出状态。如果返回 0,则代表程序正常退出,否则代表程序异常退出。有些编译器在main函数中没有添加return语句,但还是能正常编译通过是因为编译器要自动在生成的目标文件中添加return 0,但还是建议加上,提高程序的移植性。
- C++动态链接库与C动态链接库
- C调用C++动态链接库,需要把C++动态链接库用C++动态库提供的类再进行API封装
- C++调用C动态链接库,需要加上extern C,用C编译准则来编译
- 程序崩溃的一些原因
- 读取未赋值的变量
- 函数栈溢出
- 数组越界访问
- 指针的目标对象不可用
4. 函数
-
函数中值的传递方式
- 值传递
- 指针传递
- 引用传递
-
内联函数:关键字inline,可用来定义一个频繁使用的短小函数。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
- 引入内联函数的目的是为了解决程序中函数调用的效率问题,编译时进行替换,节省了运行时间。
- 内联函数要做参数类型检查, 这是内联函数跟宏相比的优势
-
虚函数(动态调用) 、内联函数(静态编译)不能同时是。
-
构造函数可以是内联函数。
-
函数指针与指针函数:
- 函数指针:函数指针为指向函数的指针,是指针变量,他与函数名无关,只与参数列表和返回类型有关。
- 指针函数:本质是函数,返回值为一个指针。
-
函数调用过程
- 说明
- 入栈操作分为两步。第一步栈顶指针自减以扩大栈帧空间;第二步,将某个寄存器的值保存新开辟的位置上。
- 出栈操作只有一步。第一步,栈顶指针自增以缩小栈帧空间,将原先最靠近栈顶的值赋予某个寄存器。
- 在堆栈中变量分布是从高地址到低地址分布。EBP是指向栈底的指针,在过程调用中不变,又称为帧指针。ESP指向栈顶,程序执行时移动,ESP减小分配空间,ESP增大释放空间,ESP又称为栈指针。
- 调用过程:
- 当函数从入口函数main函数开始执行时,编译器会将我们操作系统的运行状态,main函数的返回地址、main的参数、main函数中的变量、进行依次压栈
- 当main函数开始调用fa()函数时,编译器此时会将main函数的运行状态进行压栈,再将fa()函数的返回地址、fa函数的参数、fa定义变量依次压栈
- 当fa调用fb的时候,编译器此时会将fa函数的运行状态进行压栈,再将fb函数的返回地址、fb函数的参数、fb定义变量依次压栈。
- 当函数fb运行完成后,fb所有的压栈都会被编译器释放掉,编译器再从栈中接收到fa函数的运行状态后,衔接调用fb函数之前的操作,继续执行,同理,fa执行完后,编译器对main函数的处理也相同。
- 总结:
- 遇到函数调用时,首先参数入栈,参数入栈顺序为从右到左;
- 压入返回地址,即下一条语句地址(编译器隐含完成);
- 执行call指令,通过跳转指令,进入目标函数地址;
- 调整EBP, ESP,保护现场,寄存器入栈;
- 执行函数;
- 恢复现场,寄存器出栈,恢复EBP,ESP;
- 执行ret指令;
- 恢复EIP(编译器隐含完成)
- 出栈压入的参数
- 说明
-
为什么参数从右往左入栈
- 可以动态变化参数个数。首先,明确一点,可变参数的函数的需要指出可变参数的个数,即void fun(int count, …)。通过栈堆分析可知,自左向右的入栈方式,最前面的参数(count)被压在栈底,无法获取。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。
-
函数调用约定针对三个问题:参数传递方式;函数调用结束后的栈指针由谁恢复;函数编译后的名称
- standard call(stdcall,pascal调用约定):采用栈传递全部参数,参数从右向左压入栈;被调用函数复杂恢复栈顶指针;函数名自动加前导的下划线,后面是函数名,之后紧跟一个@符号,其后紧跟着参数的尺寸(_function@4)。用于类成员调用时,参数从右往左入栈后,this指针最后一个入栈
- cdecl(C调用约定):采用桟传递参数,参数从右向左依次入栈;由调用者负责恢复栈顶指针;在函数名前加上一个下划线前缀,格式为_function。
- fastcall:函数的第一个和第二个(从左向右)32字节参数(或者尺寸更小的)通过ecx和edx传递,其他参数通过桟传递。从第三个参数(如果有的话)开始从右向左的顺序压栈;被调用函数恢复栈顶指针;在函数名之前加上"@",在函数名后面也加上“@”和参数字节数(@function@8)。
- thiscall,唯一不能明确指明的函数修饰,因为thiscall只能用于C++类成 员函数的调用,同时thiscall也是C++成员函数缺省的调用约定。采用桟传递参数,参数从右向左入栈。如果参数个数确定,this指针通过ecx传递给被调用者;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈;对参数个数不定的,调用者清理堆栈,否则由被调函数清理堆栈。
-
Lambda函数(匿名函数):就是没有名字的函数。有一些函数只是临时用一下,而且业务逻辑也比较的简单,相当于是临时工,就没必要给它定义成一个正常函数(包含有函数名,很正式的那种)。使用临时的匿名函数,可以减轻函数的数量,让代码变的清晰易读。