static

static是C++常见的修饰符,它主要用来控制变量/函数的存储方式和可见性。

1、修饰全局变量

静态全局变量有以下特点: 1)该变量在全局区/静态区分配内存; 2)未经初始化的静态全局变量会被程序自动初始化为 0; 3)静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的

2、修饰局部变量

静态局部变量有以下特点: 1)该变量在全局区/静态区分配内存; 2)静态局部变量在程序执行到该对象的声明处时被首次初始化,即以后的函数调用不再进行初始化; 3)静态局部变量一般在声明处初始化,如果没有显式初始化,会被程序自动初始化为 0; 4)当static修饰局部变量时,该变量的生存期变为整个源程序,但是其作用域仍与auto变量相同,只能在定义该变量的作用域内使用,当离开了变量的作用域后,该静态局部变量依然存在,但是我们不能使用它,只有当再次进入其作用域时才可见,并且值不变;

3、修饰函数

静态函数有以下特点: 1)静态函数仅在本文件中可见,不能被其它文件所用; 2)其它文件中可以定义相同名字的函数,不会发生命名冲突;

4、修饰类的成员变量

类的静态成员变量有以下特点: 1)静态数据成员存储在全局区/静态区。静态数据成员定义时要分配空间,所以不能在类声明中定义,必须在类定义体外部定义; 2)对于非静态数据成员,每个类对象都有自己的拷贝。而静态数据成员被当作是类的成员。无论这个类的对象被定义了多少个,静态数据成员在程序中也只有一份拷贝,由该类型的所有对象共享访问,因此静态成员变量可以实现多个对象之间的数据共享;

5、修饰类的成员函数

类的静态成员函数有以下特点: 1)静态成员之间可以相互访问,包括静态成员函数访问静态成员变量和访问静态成员函数; 2)非静态成员函数可以任意地访问静态成员函数和静态成员变量; 3)静态成员函数不能访问非静态成员函数和非静态成员变量; 4)一个静态成员函数,它为类服务而不是为某一个类的具体对象服务,静态成员函数由于不是与任何的对象相联系,因此它不具有this指针;

const

1、修饰变量

当const修饰一个变量的时候,那么这个变量就被声明为常量。在定义该const变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;

2、修饰指针

口诀:左定值右定向; 1)对指针来说,可以指定指针本身为const;例如 :

int * const p; // 指针变量本身是一个常量,我们不可以修改指针变量的值,也就意味着我们不可以修改指针的指向。

2)也可以指定指针所指的数据为const;例如:

const int * p; // 指针所指向的内存空间不可被修改

3)或二者同时指定为const;

const int * const p; 

3、修饰函数形参、返回值

1)在一个函数声明中,const可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值; 2)有时候必须指定函数的返回值为const类型,以使得其返回值不为“左值”。

4、修饰类的成员函数

对于类的成员函数,若指定其为const类型,则表明其是一个常函数,不能修改类的成员变量;

5、修饰引用

1)const 引用的目的是:禁止通过修改引用值来改变被引用的对象。

const int &ci = 3; //正确,整型字面值常量绑定到 const引用
//  c++编译器会进行如下操作,ci绑定了一个临时量对象temp
//  int  temp  =  3 
//  const  int  &ci  =  temp;
//  我们不可以通过修改ci的值,例如 ci = 1; // error! 

2)当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值 分配空间,并将引用名作为这段空间的别名。

int i = 1;
const int &cj = i;    //正确,非常量对象绑定到 const引用
//cj=2;// 错误 cj被const修饰,不可以改变
i=3;//正确,i可以被修改,cj也被更改,cout<<cj时为3
int &cj2 = i; 
cj2=5;//正确,i被修改,cj也被更改,cout<<cj时为5

此外,当const引用和引用对象类型不一致的时候:

double i= 1.2;
const int &cj = i;    //正确,非常量对象绑定到 const引用
//cj=2.3;// 错误 cj被const修饰,不可以改变
i=3.3;//正确,i可以被修改,cj没有被更改,cout<<cj时为1
double &cj2 = i; 
cj2=5.2;//正确,i被修改为5.2,cj2被更改,为5.2,cj没有被更改,cout<<cj时为1
/* 
原因:引用变量类型为int,被引用对象类型为double。
在进行const int &cj = i;前,进行了如下操作
 double i= 1.2;
 int temp = i;
 const int &cj = temp;//所以cj并未真正绑定对象i
*/

四种类型转换

1、const_cast

作用:用于将const变量转为非const变量; 提示:const_cast中的类型必须是指针、引用或者是指向类对象成员的指针! 1)变量本身的const属性是不能去除的,但我们可以用来去除指针变量的底层const属性。但要明确const_cast只是C++的一种妥协,当存在某个指向常量的指针(底层const,不允许通过该指针改变其指向的内容),我们需要改变其指向的内容,但缺无法直接获取该变量,这时候使用const_cast可以去掉上述指针的const属性,并将其中地址赋予新指针,这个新指针就可以去更改内容,例如:

int a = 10;
const int *p1 = &a;
int *p2 = const_cast<int*>(p1);
*p2 = 11;
std::cout << a << std::endl; // 此时a被修改为11

但是如果变量a是const int a = 10;那么上述结果可能并非我们预期的那样,因为我们通过const_cast试图去修改常量,这是不被允许的,例如:

const int a = 10;
const int *p1 = &a;
int *p2 = const_cast<int*>(p1);
*p2 = 11;
std::cout << a << std::endl; // 此时a的值依然为10,因为a是常量,即使使用了const_cast也无法修改常量的值

2)const_cast其实也可以用来改变顶层const,不过是没有意义的行为。

int a = 10, b = 11;
int * const p1 = &a;
int *p2 = const_cast<int*>(p1);
p2 = &b;
std::cout << *p1 << std::endl << *p2 << std::endl; // 此时分别输出a的值和b的值,因为p1本身是一个常量,我们不可以通过const_cast去修改它的值

2、static_cast

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

// 非const转const
int a = 10;
const int b = static_cast<const int>(a);
// void*转char*
void* vptr = "123";
char* cptr = static_cast<char*>(vptr);

3、dynamic_cast

1)用于动态类型转换。只能用于含有虚函数的类(必须包含多态类类型),用于类层次间的向上或向下转换,只能转换指针或引用。

/* error
class A {};
class B : public A{};
A* pa = new B;
B* pb = dynamic_cast<B*>(pa); // error:因为不包含多态类类型!
*/
class A {
public:
	A() {};
	virtual ~A() {};
};
class B : public A{
public:
	B() {};
	~B() {};
};
A* pa = new B;
B* pb = dynamic_cast<B*>(pa);// success:包含多态类类型,可以进行动态类型转换

2)向下转换时,如果是非法的指针则返回NULL,非法的引用则抛出异常。

4、reinterpret_cast

几乎什么都可以转,比如将int转指针,可能会出现问题,尽量少用;

5、为什么不使用C风格强制转换?

C风格强制转换表面上看似功能强大什么都能转,但是转换不够明确,编译期间不能进行错误检查,使用过程中容易出现问题。

指针和引用的区别?

1)指针拥有自己的一块内存空间,而引用只是一个变量的别名。 2)使用sizeof看一个指针的大小返回是4字节(32位机器),而引用返回的则是被引用对象的大小。 3)指针可以声明和定义分离,而引用必须在声明时初始化一个已有的对象的引用。 4)引用不能为空,指针可以为空。 5)指针和引用的自增(++)运算意义不一样。 6)引用是类型安全的,而指针不是 (引用比指针多了类型检查)。

智能指针(smart pointer)

C++里面有四个智能指针:auto_ptr、shared_ptr、weak_ptr、unique_ptr,其中在C++11标准里面已经废弃了auto_ptr了。

1、为什么要使用智能指针?

智能指针的作用就是管理一个指针。我们写的程序经常会出现这种情况:程序员动态分配的内存空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度的避免这个问题,因为智能指针本质就是一个类,当超出了类的作用域的时候,类会自动调用析构函数,而析构函数又会自动释放资源,所以智能指针就实现了不需要我们手动去释放内存空间了。

2、unique_ptr

unique_ptr实现独占式拥有或严格拥有的概念,保证同一时间内只有一个智能指针可以指向该对象。

unique_ptr<std::string> p1(new std::string("auto"));
unique_ptr<std::string> p2;
p2 = p1; // error!此时编译器会报错!因为同一时间只能有一个unique_ptr指向临时对象"auto"

3、shared_ptr

shared_ptr实现共享式拥有概念,多个智能指针可以指向相同对象,该对象和其相关资源会在最后一个引用被销毁时被释放。它使用计数机制来表明资源被几个指针所共享。

shared_ptr<std::string> p1(new std::string("auto"));
shared_ptr<std::string> p2;
p2 = p1; // success!此时计数为2,因为同一时刻有两个shared_ptr指向临时对象"auto"

4、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。

为什么析构函数必须是虚函数?

将可能会被继承的父类的析构函数设置为虚函数,可以保证当我们new一个子类,然后使用基类指针指向子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。

为什么C++默认的析构函数不是虚函数?

因为将析构函数设置为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。因此C++默认的析构函数不是虚函数,而只有当需要被当做父类的时候,才设置为虚函数。

为什么构造函数不能是虚函数?

1)首先,构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功。编译器无法知道对象的实际类型,是该类本身,还是该类的一个派生类,或是更深层次的派生类,因此构造函数不能是虚函数。 2)其次,虚函数的执行依赖于虚函数表。而虚函数表在构造函数中进行初始化工作,即初始化vptr,让他指向正确的虚函数表。而在构造对象期间,虚函数表还没有被初始化,将无法进行。

虚函数和静态函数的区别?

静态函数在编译的时候就已经确定了运行时机,而虚函数则是在运行过程中进行动态绑定,并且虚函数因为用了虚函数表机制,所以调用的时候会增加一次内存开销。

重载、重写和隐藏

1)重载:两个函数名相同,但是参数列表不同(个数、类型),根据参数列表确定调用哪个函数,重载不关心函数返回类型。 2)重写(覆盖):是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。 3)隐藏:是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。

多态

1)概念:多态就是不同对象对同一行为会有不同的状态。 2)多态分为静态多态和动态多态。静态多态包含函数重载和泛型编程,在编译的时候就已经确定了运行时机;而动态多态是通过虚函数机制实现的,在运行期间动态绑定。例如,一个父类指针指向子类对象时,使用父类指针去调用子类重写了父类中的虚函数时,会调用子类重写过的函数。 3)虚函数实现:在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向了一个虚函数表,表中存放了虚函数的地址,实际的虚函数在代码段(.text)中,虚函数表在只读常量区中。当子类继承了父类的时候也会继承虚函数表,当子类重写父类中虚函数时,会将其继承到的虚函数表中的地址替换为重写的函数地址,如果子类有新增加的虚函数,按声明次序加到最后

inline函数可以实虚函数吗?

不可以,因为inline函数没有地址,无法将他存放到虚函数表中。

静态成员可以是虚函数吗?

不能,因为静态成员函数中没有this指针,使用::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

对象访问普通函数快还是虚函数快?

如果是普通对象,是一样快的,如果是指针对象或者是引用对象,调用普通函数更快一些,因为构成了多态,运行时调用虚函数要先到虚函数表中去查找。这样然后才拿到虚函数的地址,这样就不如直接可以拿到函数地址的普通函数快。

设计一个不能被继承的类

1、私有构造函数

将类的构造函数声明为私有的,这样就可以阻止子类构造对象了,实现了该类不能被继承。

class noncopyable{
private:
	noncopyable() {};
	~noncopyable() {};
};

但是这种方式同样存在一个问题,当我们想要试图去实例化这个类对象的时候,由于构造函数是私有的,因此我们无法创建这个类的对象,因此我们又可以想定义一个静态方法来构造类和释放类。

class noncopyable{
public:
	static noncopyable* construct() {
		noncopyable* m_instance = new noncopyable;
		return m_instance;
	};
	static void destruct(noncopyable* m_instance) {
		if (m_instance) {
			delete m_instance;
			m_instance = nullptr;
		}
	};
private:
	noncopyable() {};
	~noncopyable() {};
};
/* 可以通过静态方法来获取栈上的实例化对象
	noncopyable* nca = noncopyable::construct();
	noncopyable::destruct(nca);
*/

但是这个方法依然存在问题,这个类只能在堆上创建,而无法再栈上创建这个类对象。这就是私有的构造函数的局限性。

2、友元不能被继承

主要思想,设计一个辅助类base,将构造函数声明为私有的;再设计一个不能继承的类noncopyable,将noncopyable作为base的友元类。

class base {
	friend class noncopyable;
private:
	base() {};
	~base() {};
};
class noncopyable : virtual public base{
public:
	noncopyable() {};
	~noncopyable() {};
};

此刻,若一个类试图继承noncopyable类的时候,当其在调用构造函数时,不会先调用noncopyable类的构造函数,而是直接调用base的构造函数,由于该类不是base的友元类,所以无法访问。这样的话该类就不能继承noncopyable。同时因为noncopyable的构造函数是公有的,因此也可以随意的在堆栈上实例化对象。

3、final

C++11标准里面提供了一个final关键字; final关键字既可以限制类的继承,也可以禁止虚函数重写。

class noncopyable final { // 禁止被继承
public:
	virtual void work() final {}; // 禁止虚函数重写
	noncopyable() {};
	~noncopyable() {};
};

虚继承和虚基类

(转载C语言中文网:http://c.biancheng.net/view/2280.html)

1、多继承

多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。 多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示: 菱形继承:菱形继承 类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。 在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。下面是菱形继承的具体实现:

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: public A{
protected:
    int m_b;
};
//直接基类C
class C: public A{
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名冲突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}

这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类 B 和类 C 中都有成员变量 m_a(从 A 类继承而来),编译器不知道选用哪一个,所以产生了歧义。 为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:

void seta(int a){ B::m_a = a; }

这样表示使用 B 类的 m_a。当然也可以使用 C 类的:

void seta(int a){ C::m_a = a; }

2、虚继承

为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。 在继承方式前面加上 virtual 关键字就是虚继承,请看下面的例子:

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};
//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};
int main(){
    D d;
    return 0;
}

这段代码使用虚继承重新实现了上图所示的菱形继承,这样在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。 现在让我们重新梳理一下本例的继承关系,如下图所示: 虚继承解决命名冲突问题: 观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。 换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。 在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。 C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。 虚继承在C++标准库中实际应用:

3、虚基类成员的可见性

因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。 以图2中的菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性: 1)如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。 2)如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。 3)如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。 可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。

4、虚继承时的构造函数

在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。 下面我们以菱形继承为例来演示构造函数的调用:

#include <iostream>
using namespace std;
//虚基类A
class A{
public:
    A(int a);
protected:
    int m_a;
};
A::A(int a): m_a(a){ }
//直接派生类B
class B: virtual public A{
public:
    B(int a, int b);
public:
    void display();
protected:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//直接派生类C
class C: virtual public A{
public:
    C(int a, int c);
public:
    void display();
protected:
    int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
    cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}
//间接派生类D
class D: public B, public C{
public:
    D(int a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }
void D::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}
int main(){
    B b(10, 20);
    b.display();
   
    C c(30, 40);
    c.display();

    D d(50, 60, 70, 80);
    d.display();
    return 0;
}

运行结果: m_a=10, m_b=20 m_a=30, m_c=40 m_a=50, m_b=60, m_c=70, m_d=80 在最终派生类 D 的构造函数中,除了调用 B 和 C 的构造函数,还调用了 A 的构造函数,这说明 D 不但要负责初始化直接基类 B 和 C,还要负责初始化间接基类 A。而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误。 现在采用了虚继承,虚基类 A 在最终派生类 D 中只保留了一份成员变量 m_a,如果由 B 和 C 初始化 m_a,那么 B 和 C 在调用 A 的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化 m_a。 为了避免出现这种矛盾的情况,C++ 干脆规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。在第 50 行代码中,调用 B 的构造函数时试图将 m_a 初始化为 90,调用 C 的构造函数时试图将 m_a 初始化为 100,但是输出结果有力地证明了这些都是无效的,m_a 最终被初始化为 50,这正是在 D 中直接调用 A 的构造函数的结果。 另外需要关注的是构造函数的执行顺序。虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。 若改变构造函数出现的顺序:

D::D(int a, int b, int c, int d): B(90, b), C(100, c), A(a), m_d(d){ }

虽然我们将 A() 放在了最后,但是编译器仍然会先调用 A(),然后再调用 B()、C(),因为 A() 是虚基类的构造函数,比其他构造函数优先级高。如果没有使用虚继承的话,那么编译器将按照出现的顺序依次调用 B()、C()、A()。

写个函数在main函数执行前运行

1、全局变量

int func(){
	std::cout << "befor main" << std::endl;
	return 0;
}
int gloab_variable = func();

2、全局类变量

class A{
	A(){
		std::cout << "before main" << std::endl;
	}
};
A a;

new/delete与malloc/free的区别?

1)new/delete是C++的关键字;而malloc/free是C语言的库函数; 2)new返回指定类型的指针,并且可以自动计算所需空间的大小;而malloc必须要由用户自己计算所需空间大小,并在返回后强行转换为实际类型的指针; 3)malloc只管内存分配,不能对所得的内存进行初始化;而new除了分配内存处,还会对对象做初始化; 4)new如果分配失败了会抛出bad_alloc的异常,而malloc失败了会返回NULL。

拷贝构造函数的形参能否进行值传递?

不能。如果构造函数是值传递的话,当我们调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的过程又要调用拷贝构造函数.......如此循环,无法完成拷贝,直至栈满。

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

能,只不过这个过程会多调用一次拷贝构造函数。如果赋值构造函数是值传递的话,当我们调用赋值构造函数时,首先实参传递给形参,这里发生一次拷贝构造函数生成形参,然后再将形参拷贝给实参,然后形参释放,完成拷贝。

C++中struct和class的区别

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

template<class T, int i>

C++源文件从文本到可执行文件经历的过程

1、预处理阶段

对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。

2、编译阶段

将经过预处理后的预编译文件转换成特定的汇编代码,生成汇编文件

3、汇编阶段

将编译阶段生成的汇编文件转换成机器码,生成可重定位目标文件

4、链接阶段

将多个目标文件以及其所需的库链接成最终的可执行目标文件

C++的内存管理

在C++中,虚拟内存被划分为四个部分,分别为常量区、全局/静态区、堆、栈。

1、常量区

包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

2、全局/静态区

1).data段:存储程序中已初始化的全局变量和静态变量。 2).bss段:存储未初始化的全局变量和静态变量,以及所有被初始化为0的全局变量和静态变量。

3、堆区

调用new/malloc时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

4、栈区

使用栈空间存储函数的返回地址、参数、局部变量、返回值。

堆和栈的区别

1)管理方式不同:栈,由编译器自动管理,无需程序员手工控制;堆:产生和释放由程序员控制。

2)空间大小不同:栈的空间有限;堆内存可以达到4G,。 3)能否产生碎片不同:栈不会产生碎片,因为栈是种先进后出的队列。堆则容易产生碎片,多次的new/delete会造成内存的不连续,从而造成大量的碎片。 4)生长方向不同:堆的生长方式是向上的,栈是向下的。 5)分配方式不同:堆是动态分配的。栈可以是静态分配和动态分配两种,但是栈的动态分配由编译器释放。 6)分配效率不同:栈是机器系统提供的数据结构,计算机底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。堆则是由C/C++函数库提供,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

什么是内存泄漏?

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

extern

1)作为声明变量或函数的修饰符,告诉程序此变量/函数是在别处定义的,要在此处引用,extern声明不是定义,即不分配存储空间。 2)作为链接指示符,例如 extern "C"

volatile

1)在C++中,如果想把一个变量声明为volatile,就相当于告诉编译器这个变量是“易变的”,它随时可能在其他地方被修改,所以编译器不能对其做任何变化:即每次读写该变量时都必须对其内存地址直接进行操作,并且所以对该变量的操作都必须严格按照程序中规定的顺序执行。举例来说,编译器常常做的一种性能优化就是把需要频繁读取的变量缓存到寄存器中,以提升访问速度。但如果该变量的值随时都可能在片外被改变的话,那么就有可能出现被缓存的值并不是该变量的最新值的情况,从而出现运行错误。在这种情况下就需要用到volatile关键字来修饰这个变量,以确保编译器不会对该变量的读写操作进行任何的缓存优化。 2)通常中断服务程序中修改的供其他程序检测的变量需要加volatile 3)在多核多线程下,使用volatile不能保证多线程程序中共享变量的同步操作。因为C++标准下volatile并没有赋予原子性和顺序性: a. 不保证原子性:例如i++,实际上是三个操作,将内存地址读取到寄存器中,寄存器加1,再写会内存地址中。 b. 不保证顺序性:尽管volatile规定编译器不能对同一变量的所有操作进行乱序优化,但它却不能阻止编译器对不同volatile变量间的操作进行乱序优化。

构造函数和析构函数中能调用虚函数吗?

从语法上来讲,调用完全没有问题。但是从效果上看,往往不能达到所需的目的。 EffectiveC++的解释是:派生类对象的基类成分会在派生类自身成分被构造之前先构造妥当,派生类对象构造期间会首先进入基类的构造函数,在基类构造函数执行时继承类的成员变量尚未初始化,对象类型是基类类型,而不是派生类类型,虚函数会被编译器解析为基类,若使用运行时类型信息,也会把对象视为基类类型,构造函数期间调用虚函数,会调用基类自己的虚函数,此时虚函数和普通函数就没有什么区别了,达不到多态的效果。 同样的,进入基类析构函数时,对象也是基类类型。C++中派生类析构时先调用派生类的析构函数再调用基类的析构函数。一旦派生类的析构函数运行,则这个对象的派生类数据成员就被视为未定义的值,所以C++就将他们视为未定义的值,所以C++就将他们视为不再存在。假设一个派生类对象正在析构,首先调用了派生类的析构,然后再调用基类的析构时,遇到了一个虚函数,这个时候编译器有两种选择:一是编译器调用这个虚函数的基类版本,那么此时虚函数则失去了运行时调用正确版本的意义;二是调用这个虚函数的派生类版本,但是此时对象的派生类部分已经完成析构,数据成员就被视为未定义的值,这个函数调用会导致未知行为。

内联函数有什么优点?和宏定义的区别?

宏定义在预编译的时候会进行宏替换;而内联函数在编译阶段,在调用内联函数的地方进行替换,减少了函数的调用过程,但是会使得编译文件变大。因此,内联函数适合简单函数,对于复杂函数,即使定义了内联编译器也可能不会按照内联的方式进行编译。 内联函数相比宏定义更加安全,因为内联函数可以检查参数,而宏定义只是简单的文本替换。因此推荐使用内联函数而不是宏定义。使用宏定义函数要特别注意给所有单元都加上括号,例如 #define MUL(a, b) a b这种写法非常危险,应该写成 #define MUL(a, b) ((a) (b))

C++11新特性

1)关键字及新语法:auto、nullptr、for、decltype 2)STL新容器:std::array、std::forward_list、std::unordered_map、std::unordered_set 3)多线程:std::thread、std::atomic、std::condition_variable 4)智能指针内存管理:std::shared_ptr、std::weak_ptr 5)其他:std::function、std::bind和lambda表达式

深拷贝和浅拷贝

1)浅拷贝:位拷贝,拷贝构造函数,赋值重载。多个对象共用同一块资源,同一块资源释放屡次,崩溃或者内存泄漏。 2)深拷贝:每一个对象共同拥有本身的资源,必须显式提供拷贝构造函数和赋值运算符。 3)简而言之:深拷贝和浅拷贝能够简单理解为:若是一个类拥有资源,当这个类的对象发生复制过程的时候,资源从新分配,这个过程就是深拷贝,反之,没有从新分配资源,就是浅拷贝。

/*C++默认的拷贝就是浅拷贝,以下是深拷贝*/
class DeepCopy{
	public:
		DeepCopy(const DeepCopy & dc){
			this->m_data = dc.m_data;
			this->m_p = new int(*dc.m_p); 
		}
		~DeepCopy(){
			if(this->m_p){
				delete m_p;
				m_p = nullptr;
			}
		}
	public:
		int m_data;
		int *m_p;
};

可变参数模板

右值引用、移动语义和完美转发

lambda表达式

函数调用过程

如下结构的代码

int main()
{
  d = func(a, b, c);
  std::cout << d << std::endl;
  return 0;
}

调用fun()的过程大致如下: <===============main()===============> 1)参数拷贝(压栈),注意顺序是从右到左,即c-b-a; 2)保存d = func(a, b, c)的下一条指令,即std::cout << d << std::endl;(实际上是这条语句对应的汇编指令的起始位置); 3)跳转到func()函数,注意,到目前为止,这些都是在main()中进行的; <===================================>

<===============func()================> 4)移动ebp、esp形成新的栈帧结构; 5)压栈(push)形成临时变量并执行相关操作; 6)return一个值; 7)出栈(pop); 8)恢复main函数的栈帧结构; 9)返回main函数; <===================================>

i++和++i的实现

//i++实现代码为:
int operator++(int)
{
    int temp = *this;
    ++*this;
    return temp;
}//返回一个int型的对象本身

// ++i实现代码为:
int& operator++()
{
    *this += 1;
    return *this;
}//返回一个int型的对象引用