总结c++八股知识

一、C++对象模型相关知识

声明:这篇总结关于c++对象模型的知识主要来源于这篇笔记的总结:《深度探索C++对象模型》读书笔记,
作者邮箱 chuanqi.tan@gmail.com

C++ virtual机制或技术

  • C++ 的成本来源就是 vitrual 机制,无 virtual就无额外的开销,也就是说当定义一个平坦类型的类,跟定义位于该类命名空间下的变量和函数等同,与C的struct定义的结构体一致。
  • Virtual function机制则由以下的2个的两个步骤来支持:
    ◦ 当定义一个class时,每一个class产生出一系列指针Virtual function的指针,放在一个被称为virtual table(vtbl, vtable)的表格中;
    ◦ 当实例化一个class object时,每一个class object被添加了一个指针vptr,指向相对应的vtable。vptr的设置由编译器全权负责,不同的编译器的实现模型略有差异,vptr主流的编译器的放在头地址,不过也有的放在尾地址,对于虚基类,处理会更复杂,但使用上程序员无需关心。传统上,vptr被安放在所有被明确声明的member的最后,不过也有些编译器把vptr放在最前面(MSVC++就是把vptr放在最前面,而GCC是把vptr放在最后面)。
  • RTTI: 一般来说,每一个 class 相关联的 type_info 对象的指针 通常也保存在 vtable 的第一个 slot 中。
  • 需要清楚的明白一点是:一个 vtable 对应 一个 class , 一个 vptr 才对应 一个 class object,必须区分开这2个概念。

C++ 的对象模型成员内存分布

在这里插入图片描述

  • Nonstatic data members被配置于每一个class object之内;

  • Static data members则被存放在所有的class object之外;

  • Static和Nonstatic function members也被放在所有的class object之外。

  • 一个对象的内存布局大小通常由3部分组成:

    1. 其nonstatic data member的总和大小;
    2. 任何由于位对齐所需要的填补上去的空间;
    3. 加上了为了支持virtual机制而引起的额外负担。

指针的本质

  • 对于内存来说,指针都是占用一个机器码大小的地址,地址是一个数字,这个数字代表指针内存中的一个位置;
  • 指针的类型只是用来告诉编译器分配对应的内存大小;
  • 转型操作也只是一种编译器的指令,它改变的内是编译器对被指内存的解释方式而已,类型操作函数reinterpret_cast充分解释这个观点。

多态只能由”指针“或”引用“来实现的根本原因

  • 根据上面指针的本质所述,因为所有指针的大小一样,注意,引用的实现是用指针实现的,只不过引用在实现上增加了一些限制,而且有一些特定的场景需要用引用。所以,对于有虚指针的父类和子类,子类的虚指针能够赋值给父类的虚指针,从而能通过父类的虚指针调用子类的虚函数表实现多态。
  • 这里注意,根据父类指针的解释范围可知,子类自己定义的变量,父类无法直接调用的,但是可以通过多态调用子类的函数间接使用。
  • 在初始化、 assignment 等操作时,编译器会保证对象的 vptrs 得到正确的设置。这是编译器的职责,它必须做到。一般都是通过在各种操作中插入编译器的代码来实现的。

产生默认构造函数的4个时机

  • 默认构造函数不是一定会产生,当类是trivial的时,不会产生,当是nontrival是会产生。

  • 这个nontrivial 的默认构造函数只满足编译器的需要:

    • 调用member objects的默认构造函数
    • base class的默认构造函数
    • 初始化virtual function(包括虚指针)
    • virutal base class机制
  • 其它情况时,类在概念上拥有默认构造函数,但是实际上根本不会被产生出来(前面的区分trivial和notrivial)。

产生默认拷贝函数的4个时机

  • 对于大部分的class来说,拷贝构造函数仅仅需要按位拷贝就可以。 满足 bitwise copy semantics 的拷贝构造函数是 trivial 的,就不会真正被合成出来(与默认构造函数一样,只有 nontrivial 的拷贝构造函数才会被真正合成出来)。
    1. 当class内含一个member object而后者声明了(也可能由于nontrivial语意从而编译器 真正合成出来的)一个copy constructor时;
    2. 当class继承自一个存在有copy constructor的base class(同样也可能是合成)时;
    3. 当class声明了一个或多个virtual functions时;(vf影响了位语意,进而影响效率)
    4. 当class 派生自一个继承串链,其中一个或多个virtual base classes时。
  • 可以发现,与默认构造函数是一一对应的。

产生默认赋值拷贝操作符的4个时机

以下情况不会表现出bitwise copy语意:

  • 当含有一个或以上的成员有copy assignment operator时;
  • 当基类有copy assignment operator时;
  • 当class中有virtual function时(需要正确设置vptr);
  • 当class的继承体系中有virtual base class时;

产生默认析构函数的2个时机

  • 当class内含的menber object拥有destructor时;
  • 当base class 拥有destructor时。
  • 注意,即使拥有虚函数时,只要编译器并不会合成出一个destructor;甚至虚继承,虚继承基类没有声明destructor是,也不会合成一个destructor。

C++隐式生成的4大成员函数,要不要自己去声明

  • 不要。因为如果是trivial的,这些函数不会被真正的合成出来(只存在于概念上),当然也就没有调用的成本了,去提供一个trivial的成员反而是不符合效率的。

类变量的初始化顺序

  • 按照变量的声明顺序初始化,所以注意,变量的初始化的顺序与初始化列表的顺序无关,有些编译器会对错误初始化列表顺序提出警告。
  • 编译器会一一操作初始化列表,把其中的初始化操作以member声明的次序在constructor内安插初始化操作,并且在任何explicit user code 之前。
  • “ 以 member 声明的次序来决定初始化次序”和“初始化列表中的排列次序”之间的 外观错乱,可能会导致一些不明显的Bug。

类声明头文件可以被许多源文件所包含,如何避免合成默认构造函数、拷贝构造函数、析构函数、赋值拷贝操作符(4大成员函数)时不引起函数的重定义?

  • 解决方法是以inline的方式完成,如果函数太复杂不适合inline,就会合成一个explicit noninlinestatic实体(Static函数独立于编译单元)。

空类的大小

  • 空类也有 1Byte 的大小,因为这样才能使得这个 class 的 2 个 objects 在内存中有独一无二的
    地址。

派生类的成员和基类成员的排列次序

  • 这点并未在C++ Standard中强制指定;理论上编译器可以自由安排, 但是对于大部分的编译器实现来说,都是把基类成员放在前面,但是 virtual baseclass 除外。(一般而言,任何一条规则一旦碰到virtual base class就没辙了)。

单一继承并含有虚拟函数时的内存布局(考虑一般把vptr放在尾部的设计):

在这里插入图片描述

  • 单一继承时 vptr 被放在第一个子类的末尾,产生这样布局的根本原因在于“ 基类的完整性必须在子类中得以保存 ”。
  • 这个名叫vptr_Point2d的vptr可以这样理解,由Point2d而引发的vptr,在Point2d的对象中,这个vptr_Point2d所指向的是与Point2d所对应的point2d_vtable,而在Point3d的对象中,这个vptr_Point2d所指向的却是与Point3d所对应的point3d_vtable。

多重继承时的内存布局(多重继承时的主要问题在于派生类与非第1基类之间的转换)

在这里插入图片描述

  • 在多重继承的派生体系中,将派生类的地址转换为第1基类是成本与单继承是相同的,只需要改换地址的解释方式而已;而对于转换为非第1基类的情况,则需要对地址进行一定的offset操作才行。
  • C++ Standard 并未明确 base classes 的特定排列次序,但是目前的编译器都是按照声明的次序来安放他们的。(有一个优化:如果第1基类没有vtable而后继基类有,则可能把它们调个位置)。
  • 多重继承中,可能会有多个vptr指针,视其继承体系而定:派生类中vptr的数目最等于所有基类的vptr数目的总和。

虚拟继承

  • 虚拟继承把一个类切割为 2 部分:一个不变局部和一个共享局部。

  • 一种做法是在vtable中放置virtual base class的offset:

    -

  • 一种做法是直接使用一个指针指向虚基类:

    这里是引用

  • 两种方法对比:

    这种使用偏移地址的方式好处在于: vptr 是已经存在的成本,而 vtable 是 class 的所有objects
    所共享的成本。对于每一个 class object 没有引入任何的额外成本,仅仅在 vtable 多存储了一个 slot
    布局,而前一种方式却对每一个 object 都引了两个指针的巨大成本。
    这两种方式都是把虚基类放在内存中模型中的最后面,然后借由一层间接性(指针或offset)来访问。

c++函数的设计准则

  • nonstatic member function 至少必须和一般的 nonmemberfunction 有相同的效率。
  • 实际上,nonstatic member function会被编译器进行如下的转换,变成一个普通函数:

    Type1 X::foo(Type2 arg1) { … } 会被转换为如下的普通函数:
    void foo(X *const this, Type1 &__result, Type2 arg1) { … }

  • 实际上,普通函数、普通成员函数、静态成员函数到最后都会变成与C语言函数类似的普通函数,只是编译器在这些不同类型的函数身上做了不同的扩展,并放在不同的scope里面而已。

inline 内联函数的好处

  • 函数性能测试表明, inline 函数的性能如此之高,比其它类型的函数高的不是一个等级。因为inline函数不只能够节省一般函数调用所带来的额外负担,也给编译器提供了程序优化的额外机会。
  • 当用关键字标识inline时,编译器不一定执行内联扩展操作,一般处理一个inline函数有两个阶段:

    分析函数定义,如果函数因其复杂度,或因其建构问题,被判定不可成为inline,它会被转为一个static函数,并在“被编译模块”内产生对应的函数定义。
    真正的inline函数扩展操作是在调用的那一点上。
    是C中#define宏定义的一个安全的替代品

静态成员函数的好处

  • 静态成员函数其实就是带有类scope的普通函数,它也没有this指针,所以它的地址类型并不是一个指向成员的指针而仅仅是一个普通的函数指针而已。
  • 静态成员函数是作为一个callback的理想对象,在类的scope内,又是普通的函数指针。

取一个member function的地址

  • 如果该函数是nonvirtual,则得到的结果是它在内存中的真正地址。然而这个值是不完全的,它需要被绑定于某个class object的地址上,才能够通过它调用该函数。很明显,这个nonstatic member function被编译器添加了一个参数this,如果不绑定于class object就无法传递这个this指针:(origin.*fptr)(); 会被转换为 (fptr)(&origin);
  • 然而,对于一些成员函数,经过测试,不传递this指针也能正确执行,原因不详。
  • 对一个 virtual member function 取其地址,所能获得的只是一个 vtable 中的索引值。

纯虚函数

class A{
  public:
	virtual ~A(){}
	virtual void f() = 0;
};

void A::f() {cout << "pure virtual" << endl;} //纯虚函数必须定义在类声明的外部

class D : public A{
  public:
	virtual void f(){ A::f(); } //纯虚函数必须经由派生类显式的要求调用
};
int main() {
	D d;
	d.f();
	return 0;
}
  • 纯虚函数不能在类的声明中提供实现,只能在类声明的外部来提供默认的实现;
  • 基类的纯虚函数的默认实现必须由派生类显式的要求调用;
  • 派生类不会自动继承这个纯虚函数的定义,如果派生类D未定义f(),那么D依然是一个抽象类型;
  • 这种pure virtual函数还提供实现的方案比较好的应用场景为:基类提供了一个默认的实现,但是不希望自动的继承给派生类使用,除非派生类明确的要求。
  • 还需要注意这个纯虚函数为析构函数的情况。C++语言保证继承体系中的每一个class object的destructors都会被调用。所以编译器一定会扩展派生类的析构函数去显式地调用基类的析构函数。
  • 另外一个重要的应用场景:有些情况下会把析构函数声明为纯虚。这时,必须为纯虚析构函数提供一个默认的实现。否则,派生类的析构函数由于编译器的扩展而显式的调用基类的析构函数时会找不到定义。同时编译器也无法为已经声明为纯虚的析构函数生成一个默认的实现。

POD的class

  • 对于可以视为POD的class( 没有声明构造函数、没有 virtual 机制等等),就可以使用POD结构特有的initialization list进行初始化。

    Point p = {2, 3};
    
  • C++11中的initialization list被大量使用。

引入virtual function会给对象的构造、拷贝和析构等过程带来的负担

  • constructor必须被安插一些代码以便将vptr正确的初始化,这些代码需要被安插在任何base class constructors的调用之后,但必须在任何user code的代码之前;
  • 合成copy constructor和copy assignment operator,因为它们不再是trivial的了,它们必须安插代码以正确的设置vptr;

在父类构造函数中调用virtual function有没有多态性

  • 没有。在子类的构造函数中构造父类,而在父类的构造函数调用虚函数时,此时子类对象还不完整,子类的部分还没有开始构造,当然不能调用它们的成员函数,否则在它们的成员函数中可能会访问还不存在的成员变量。
  • 那么是如何解决这个问题的?这与构造顺序有关,父类的vptr设置的是指向自己的virtual table,而子类的vptr要等待所有父类构造完才会构造,显然此时子类还未形成。
  • 先调用所有基类的构造函数,再设置 vptr ,然后再调用 member initialization 操作。这是构造函数中没有多态性的根本原因!

编译器扩张构造函数的顺序

  • 初始化成员:使用member initialization list (有初始化列表一定人为定义了构造函数)或者调用成员变量的默认构造函数(有参数的成员变量必须在member initialization list显式调用);
  • 在那之前,如果class object有vptr,它们必须被正确的设置;
  • 在那之前,所有的上一层的base class construcotrs必须被调用,以base classes声明的顺序。使用member initialization list 或者调用成员变量的默认构造函数,同时如果base class是多重继承下的非第1基类,还需要调整this指针;
  • 在那之前,所有的virtual base class constructors 必须被调用,从左到右,从深到浅。并同时设置好virtual base class所需要使用的各种机制;
  • 即处理顺序为:virtual base classes → base class → vptr → member。

析构函数的执行顺序

  • 顺序如下:

    • 如果object内带有vptr,那么首先重设相关的vtable
    • destructor函数本身现在会被执行, 也就是说 vptr 会在程序员的代码执行之前被重设;
    • 以声明顺序的相反顺序调用members的析构函数;
    • 如果有任何直接的(上一层)nonvirtual base classed 拥有destructor,那么会以其声明顺序的相反顺序被调用;
    • 如果有任何virtual base classes拥有destructor,而当前讨论的这个class是最尾端的,那么它们会以其原来的构造顺序的相反顺序被调用。
  • 由于析构函数中的重设 vptr 会在任何代码之前被执行,这样就保证了在析构函数中也不具有多态性,从而不会调用子类的函数。因为此时子类已经不完整了,子类中的成员已经不存在了,而子类的函数有可能需要使用这些成员。

  • 构造函数和析构函数中都不具有多态性:这并不是语言的弱点,而是正确的语意所要求的(因为那个时候的对象不完整)。

C++的多态与RTTI

  • 由于具备多态性质的class都已经含有一个vptr指向vtable了,C++把类型信息放在vtable的第1个slot中(一个type_info的指针指向一个表示当前类型的type_info对象),从而几乎没有付出代价的支持了RTTI(1byte per class, not 1byte per class object)。
  • 由于 RTTI 所需要的信息放在 vtable 中,自然的:只有含有vptr的类才支持RTTI。
  • 有了RTTI机制的支持,就可以实施保证安全的动态转型操作dynamic_cast<>();

在dynamic_cast中使用指针和引用的区别在于当转型失败时:

  • 指针版本会返回0,使用者需要进行检查;
  • 引用的版本会抛出一个bad_cast exception(因为没有空引用啊);
  • 这两种机制各有用处吧,视需求而用。

type_info类型

  • type_info类型的copy构造函数和operator=操作符都被声明为私有,禁止了赋值和拷贝操作。而且只提供了一个受保护的带有一个const char *参数的构造函数,因为不能直接得到type_info 对象,只能通过 typeid() 运算符来得到这类对象。
RTTI用于多态与非多态的差别
  • RTTI只适用于多态类型(RTTI信息存于vtable的原因),事实上type_info object也适用于非多态类型。typeid()使用于非多态类型时的差异在于,这时候的type_info object是静态取得的(编译器直接给扩展了),而非像多态类型一样在执行期通过vtable动态取得。

  • 这之间的区别看下面的这个例子就会很快明白了:

    struct B : public A{}; //B也是非多态类型 
    int main() { 
    	A *pa = new B; 
    	cout << typeid(pa).name() << endl; 
    	cout <<
    typeid(*pa).name() << endl; }
    

    将输出:
    Struct A *
    Struct A
    这没有检测出pa所指的真正类型,原因就在于 typeid 运算符 用在非多态类型上时,会 被编译器在编译 期间 静态的扩展了。
    也许是类似的扩展:
    typeid(pa).name() => typeid(A*).name()
    typeid(*pa).name() => typeid(A).name()
    如果给struct A添加一个虚拟函数,从而 使得类型 A 和 B 都变成多态类型,于是 typeid 运算符就会在运行期间动态的去获取 它们的真正类型了。

    virtual ~A(){} //A包含了一个虚函数,从而把A变成了多态类型 	}; 	
    struct B : public A{}; //B从A继承了一个虚函数,所以也是多态类型 	
    int main() {
     	A *pa = new B;
    	cout << typeid(pa).name() << endl;
    	cout << typeid(*pa).name() <<vendl; 	
    } 	
    

    将输出:
    Struct A *
    Struct B
    因为pa 是一个指针,所以肯定返回指针类型,但对于*pa,这是一个对象,这个对象里的vptr指向的virtual table 的type_info是原本的B。

二、C++杂项知识点

c++ static关键字的作用

这部分详细的知识查看我的其他博文
静态初始化可以看这篇文章
static关键字使用恰当能够大大提高程序的模块化特性,有利于扩展和维护。

  • 静态局部变量:变量在全局数据区分配内存空间;第一次调用时自动对其初始化,其作用域为局部作用域,当定义它的函数结束时,其作用域随之结束。
  • 静态全局变量:仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。(**区别全局变量:**定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量。需要执行静态初始化))。需要执行静态初始化
  • 全局静态函数:全局函数的作用域被限制在当前文件内。
  • 静态类成员变量:存储在数据段(全局数据区),所有实例中都只有一份,两种访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>。非const静态类变量必须类外初始化。需要执行静态初始化
  • 静态类成员函数:与静态类数据成员类似,静态成员函数属于整个类,可以通过类调用,但是此时没有this指针,只能调用静态成员变量。出现在类体外的函数定义不能指定关键字static。

C++11初始化局部静态变量是线程安全的,C++98则不

  • C++11起,Singleton 最佳实现是静态局部对象的方法,该方法是线程安全的。C++11标准保证:如果多个线程试图同时初始化同一静态局部对象,则初始化严格只发生一次。

struct和class关键字的意义

  • 使用struct也可以实现继承多态,那我们还要class关键字又什么用呢唯一区别就是:struct与class唯一不同的是struct默认的关键字是public, class默认的关键字是private。使用class更好的进行权限访问控制,当然struct也可以设置关键字为private。
  • 在编程思想上,struct用来表现那些只有数据的集合体POD(Plain OI’ Data)、而class则希望表达的是ADT(abstract data type)的思想;

C++之访问控制与友元

  • public 所有均可访问
  • private 类自己的成员函数访问,不能被类对象访问
  • protected 类自己以及子类访问,不能被类对象访问
  • 类中省略public 等时,默认是private属性
  • 继承基类, 在基类前省略public 等时, 默认是protected属性
  • friend 友元,别人是你的朋友,他可以访问我的东西。(但不是我可以访问他的东西)
  • 当一个类作为另一个类的友元时,它的所有成员函数都是另一个类的友元函数,都可以访问另一个类的私有或者公有成员
  • friend的授权在编译期

指针常量与常量指针

1、指针常量——指针类型的常量(int *const p)

  • 本质上一个常量,指针用来说明常量的类型,表示该常量是一个指针类型的常量。在指针常量中,指针自身的值是一个常量,不可改变,始终指向同一个地址。在定义的同时必须初始化。
  • 函数名、数组名就是指针常量

2、常量指针——指向“常量”的指针(const int *p, int const *p)

  • 常量指针本质上是一个指针,常量表示指针指向的内容是“常量”,意思是p*是右值。

C++ 中拷贝赋值函数的形参能否进行值传递?

在拷贝构造函数是采用应用的方式传递时,答案是能,但会多以拷贝。否则不能。

C++ 中拷贝函数的形参能否进行值传递?

不能。会无线递归调用。

定义了拷贝构造函数时,则必须定义构造函数

原因不详。

三、编译与构建

编译过程与链接过程

在VC或VS上编写完代码,点击编译按钮准备生成exe文件时,编译器做了两步工作:

第一步,编译过程,将每个.cpp(.c)和相应的.h文件编译成obj文件;
第二步, 链接过程,将工程中所有的obj文件进行LINK,生成最终.exe文件。
如下图:
在这里插入图片描述

那么,错误可能在两个地方产生:

一个,编译时的错误,这个主要是语法错误;
一个,链接时的错误,主要是重复定义变量等。

编译单元(模块)

编译单元指在编译阶段生成的每个obj文件(目标文件:linux:.o ; win: .obj):

一个obj文件就是一个编译单元。 一个.cpp(.c)和它相应的.h文件共同组成了一个编译单元。
一个工程由很多编译单元组成,每个obj文件里包含了变量存储的相对地址等。

编译过程

编译过程包含如下三个步骤:
在这里插入图片描述

  • 预处理:读取c/c++源程序,对其中的伪指令(以# 开头的指令)和特殊符号进行处理。经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及C语言的关键字,如main, if , else , for , while , { , } , + , - , * , \ 等等。
  • 编译:编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
  • 汇编:汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。

动态链接与静态链接

  • 静态链接:在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。
  • 动态链接:函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。

声明与定义

  • “声明”则只是声明这个符号的存在,即告诉编译器,这个符号是在其他文件中定义的,我这里先用着,你链接 的时候再到别的地方去找找看它到底是什么吧。
  • “定义”就是把一个符号完完整整地描述出来:它是变 量还是函数,返回什么类型,需要什么参数等等。
  • 函数或变量在声明时,并没有给它实际的物理内存空间,它有时候可保证你的程序编译通过;
  • 函数或变量在定义时,它就在内存中有了实际的物理空间。
  • 如果你在编译单元中引用的外部变量没有在整个工程中任何一个地方定义的话,那么即使它在编译时可以通过,在连接时也会报错,因为程序在内存中找不到这个变量。
  • 函数或变量可以声明多次,但定义只能有一次。

头文件

这部分看这篇文章。下面做简要总结:

  • 头文件不参与编译

  • 头文件只起声明作用

  • 头文件不应当有定义,除了以下三种情况:

    • 头文件中可以写const对象的定义。因为全局的const对象默 认是没有extern的声明的,所以它只在当前文件中有效。同理,static对象的定义也可以放进头文件。
    • 头文件中可以写内联函数(inline)的定义。因为inline函数是需要编译器在遇到它的地方根据它的定义把它内联展开的,而并非是普通函数那样可以先声明再链 接的(内联函数不会链接),所以编译器就需要在编译时看到内联函数的完整定义才行。
    • 头文件中可以写类 (class)的定义。因为在程序中创建一个类的对象时,编译器只有在这个类的定义完全可见的情况下,才能知道这个类的对象应该如何布局,所以,关于类的 定义的要求,跟内联函数是基本一样的。
  • 考 虑一下,如果头文件中只包含声明语句的话,它被同一个.cpp文件包含再多次都没问题——因为声明语句的出现是不受限制的。然而,上面讨论到的头文件中的 三个例外也是头文件很常用的一个用处。那么,一旦一个头文件中出现了上面三个例外中的任何一个,它再被一个.cpp包含多次的话,问题就大了。因为这三个 例外中的语法元素虽然“可以定义在多个源文件中”,但是“在一个源文件中只能出现一次”。设想一下,如果a.h中含有类A的定义,b.h中含有类B的定 义,由于类B的定义依赖了类A,所以b.h中也#include了a.h。现在有一个源文件,它同时用到了类A和类B,于是程序员在这个源文件中既把 a.h包含进来了,也把b.h包含进来了。这时,问题就来了:类A的定义在这个源文件中出现了两次!于是整个程序就不能通过编译了。你也许会认为这是程序 员的失误——他应该知道b.h包含了a.h——但事实上他不应该知道。

    使用"#define"配合条件编译可以很好地解决这个问题。在一 个头文件中,通过#define定义一个名字,并且通过条件编译#ifndef…#endif使得编译器可以根据这个名字是否被定义,再决定要不要继
    续编译该头文中后续的内容。这个方法虽然简单,但是写头文件时一定记得写进去。

模板类的存放方式

  • C++中模板的声明和实现能分离:
    • 显式实例化所需的所有模板实例
    • 只是在主程序中#include的是相应的***.cpp包含进来(本来是#include ***.h), 相当于把定义的模板方式(注意不是定义,是模板,切记,定义只能有一次)展示出来。
  • C++中模板的声明和实现最好不要分开,都写在.h文件,这是因为在多个cpp文件中引用模板参数时可能引起重复定义的编译错误。

为什么模版类的声明和实现分开会出现连接问题

  • 因为需要单独编译,并且模板是实例化样式的多态性;在实例化模板时,编译器会使用给定的模板参数创建一个新类。
  • 因此,编译器需要访问方法的实现,以使用template参数实例化它们。如果这些实现不在头文件中,则将无法访问它们,因此编译器将无法实例化模板。
  • 类模板不是类,而是为T我们遇到的每个类创建新类的秘诀。
  • 模板不能被编译成代码,只有实例化模板的结果可以被译。

内联函数的声明和定义位置

  • 在C++的类中,如果函数成员在类的定义体中被定义,那么编译器会视这个函数为 内联的(注意一下,如果把函数成员的定义写在类定义的头文件中,而没有写进类定义中, 这是不合法的,因为这个函数成员此时就不是内联的了。一旦头文件被两个或两个以上的.cpp文件包含,这个函数成员就被重定义了。)。类的成员函数为内联函数的情况;

    1、在类内中加inline声明,在类外也加inline进行定义,编译通过。
    2、在类内中加inline声明,在类外不加inline进行定义,编译通过。
    3、在类内中加inline声明,同时进行定义,编译通过。
    上面前两种情况在写头文件(类的声明)和类的实现时,特别注意:必须把声明和实现同时写在头文件中,不能分开写。
    但具体是不是按内联执行则由编译器根据函数的特性来定。

  • 普通函数为内联函数:

    关键字inline 必须与函数定义体放在一起才能使函数成为内联,仅将inline 放在函数声明前面不起任何作用。
    最好将内联函数定义放在头文件中。不然,你需要为每个调用该内联函数的文件提供一次该内联函数的实现,并得保证不同文件的实现一致,不一样的话,一般编译器会报错。

全局变量

全局变量属于静态变量!!!!所谓静态变量是指:其声明周期是static的, 即:自其初始化就一直存在, 直至程序结束才生命over的那些变量!

  • 需要静态初始化,分为:
    • 编译时初始化:如果初始化值是常量并且静态变量本身是基本数据类型(POD)。
    • 运行时初始化:又分为
      • 加载时初始化:指的是在程序被加载时立即进行的初始化. 这个初始化发生在main函数之前,系统会自动生成Startup函数初始化,此时是单线程运行,不存在线程同步问题。由于这个初始化是在程序加载时一次过对变量进行初始化, 而即使程序任何地方都没访问过该变量, 仍然会发生, 因此形象地称之为"饿汉式初始化". 类静态变量的初始化初始化属于这一类。
      • global variables 和 static variables with file scope 的初始化一定是加载时初始化.
      • 调用时初始化:static variables with block scrope 一定是运行时初始化. 这个初始化发生在这个变量第一次被引用时, 也就是说, 从程序执行模型角度看, 程序所在进程空间中, 哪个线程先访问了这个变量, 就是哪个线程来初始化这个变量!!!
      • 因此, 相对于加载初始化来说, 这种初始化是把真正的初始化动作推迟到第一次被访问时, 因而形象地称为"懒汉式初始化".
      • C++11初始化局部静态变量是线程安全的,C++98则不。
  • 需要在其他文件调用普通全局变量时,做法是在.h文件用extern声明,再在.cpp文件定义,然后在需要用到的地方包含对应的.h文件就好了,但不能包含.cpp文件,这样会导致重新定义全局变量,此时编译会报重复定义错误。

全局变量初始化时机

  • C++保证:全局变量会在第一次用到之前构造好,在main()结束之前析构掉。
  • C++程序中所有的Global object都放置在程序的data segment中,如果显式的指定它一个初值,此object将以该值为初值,否则object所配置到的内存内容为0,但是它的constructor在程序激活时才会被调用。

局部静态变量初始化时机

  • 现在的 C++ Standard 已经强制要求局部静态对象在第一次被使用时才被构造出来。
  • 这也是Effective C++中Singleton手法所利用的。
  • 而且在程序结束时会被以构造的相反次序被摧毁。

四、C++标准库知识点

待续。。。

### C++ 面试常见问题总结及常用知识点 #### 1. 基类的析构函数为何要声明为虚函数? 在面向对象编程中,如果一个类可能被继承,并且该类的对象可能会通过基类指针或引用进行删除,则其析构函数必须是虚函数。否则,当通过基类指针删除派生类对象时,只会调用基类的析构函数而不会调用派生类的析构函数,这会导致资源泄漏[^1]。 #### 2. `extern` 的作用 `extern` 关键字用于声明变量或函数是在别处定义的,需要在此处引用。在 C++ 中,若需调用 C 库函数,则需要用 `extern "C"` 来声明这些函数,以告知编译器按照 C 语言的方式处理符号链接,从而避免由于 C++ 名称修饰(name mangling)带来的链接错误[^2]。 #### 3. 拷贝构造函数的作用 拷贝构造函数是一种特殊的构造函数,用于创建一个新对象作为现有对象的副本。它通常接受一个与自身类型相同的常量引用作为参数。例如: ```cpp class MyClass { public: MyClass(const MyClass& other); // Copy constructor }; ``` 拷贝构造函数会在以下几种情况下自动调用:按值传递对象给函数、从函数返回对象、显式地复制对象等。正确实现拷贝构造函数对于管理资源非常重要,尤其是在涉及深拷贝和浅拷贝的情况下[^2]。 #### 4. 多线程环境下的资源管理 在多线程环境中,对共享资源的操作需要特别小心。例如使用智能指针如 `std::shared_ptr` 管理公共资源时,读写操作应该同步以防止数据竞争。多个线程同时访问同一个 `std::shared_ptr` 实例时,即使只是读取也可能引发未定义行为,因为内部计数器更新不是原子操作。因此,建议使用互斥锁或其他同步机制来保护这类操作[^3]。 #### 5. 头文件保护宏 为了防止头文件被多次包含,可以使用预处理器指令 `#ifndef`, `#define`, 和 `#endif` 或者直接使用 `#pragma once` 指令。两者都能达到类似的效果,但 `#pragma once` 更加简洁易用,不过需要注意的是并非所有编译器都支持这一特性[^4]。 #### 6. 结构体与联合的区别 - **结构体 (`struct`):** 将不同类型的数据组合成一个整体,每个成员都有独立的存储空间。 - **联合 (`union`):** 所有成员共享同一段内存区域,任何时候只有一个成员有效。适用于节省内存需求的应用场景[^4]。 #### 7. 虚函数表(vtable)和虚函数指针(vptr) 虚函数机制依赖于虚函数表和虚函数指针。每个具有虚函数的类都有一个虚函数表,其中包含了所有虚函数的地址;每个对象则包含一个指向其所属类虚函数表的指针(即 vptr)。这样,在运行时可以通过 vptr 找到正确的虚函数表,并据此调用相应的虚函数,实现了动态绑定或多态性。 #### 8. RAII 技术 RAII (Resource Acquisition Is Initialization) 是一种编程技术,用来管理资源的生命周期。基本思想是将资源获取与释放绑定到对象的构造与析构上,确保即使发生异常也能安全释放资源。标准库中的智能指针如 `std::unique_ptr` 和 `std::shared_ptr` 就很好地体现了这一点。 #### 9. 内存泄漏检测 了解如何利用工具(如 Valgrind, Visual Leak Detector)以及手动编写代码检查内存泄漏的方法是非常重要的。此外,理解智能指针和其他现代 C++ 特性的使用可以帮助减少手动内存管理的需求,进而降低出错概率。 #### 10. STL 容器与算法 熟悉标准模板库(STL)提供的各种容器(vector, list, map, set 等)及其适用场合,掌握常用算法(sort, find, transform 等),能够高效地解决问题。同时要注意迭代器失效的问题以及不同容器之间的性能差异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值