C++面试题总结

C++继承

A类继承B类之后,A类就具有了B类的部分成员,具体得到了哪些成员,这得由两个方面决定:继承方式、基类成员的访问权限。

A类B类(A的派生类)C类(B的派生类)

公有继承
公有成员公有成员公有成员
私有成员(无)(无)
保护成员保护成员保护成员
私有继承公有成员私有成员(无)
私有成员(无)(无)
保护成员私有成员(无)
保护继承公有成员保护成员保护成员
私有成员(无)(无)
保护成员保护成员保护成员

C++继承中关于子类构造函数的写法:

  1. 如果子类没有定义构造方法,则调用父类的无参数构造方法。

  2. 如果子类定义了构造方法,不论是无参数还是带参数,在创建子类的对象的时候,首先执行父类无参数的构造方法,然后执行自己的构造方法。

  3. 在创建子类对象的时候如果子类的构造函数没有显式调用父类的构造函数,则会调用父类的默认无参构造函数。

  4. 在创建子类对象的时候,如果子类的构造函数没有显式调用父类的构造函数,且父类自己提供了无参构造函数,则会调用父类自己的无参构造函数。

  5. 在创建子类对象的时候,如果子类的构造函数没有显式调用父类的构造函数且父类只定义了自己的有参构造函数,则会出错(如果父类只有有参数的构造方法,则子类必须显式调用带参构造函数,或者给父类加一个默认构造函数)。

#include<iostream>
using namespace std;

class animal
{
public:
	animal(int height, int weight)
	{
		cout << "animal construct" << endl;
	}

};

class fish :public animal
{
public:
	fish() :animal(400, 300)
	{
		cout << "fish construct" << endl;
	}
};

int main()
{
	fish fh;
}

在fish类的构造函数后,加一个冒号“:”,然后加上父类的带参构造函数,这就是上面提到的显式调用父类的构造函数。这样,在子类的构造函数被调用时,系统就会去调用父类的带参数的构造函数去构造对象。

这种初始化方式,还常用来对类中的常量const成员进行初始化,如:

class point
{
public:
	point() : x(0), y(0) {}
private:
	const int x;
	const int y;
};

其实,我们也可以自己给子类写自己的(有参数的)构造函数体,而非调用父类的(有参数的)构造函数。如:

class Shape
{
protected:
	int width, height;
public:
	Shape() {};
	Shape(int a, int b)
	{
		width = a;
		height = b;
	}
	virtual int area()
	{
		cout << "Parent class area :" << endl;
		return 0;
	}
};
class Rectangle :public Shape
{
public:
	Rectangle(int a, int b)
	{
		width = a;
		height = b;
	}
	virtual int area()
	{
		cout << "Rectangle class area :" << endl;
		return (width * height);
};

但这样的话,父类必须要有一个默认构造函数,否则会报错,因为必须先执行父类的构造函数。
关于为什么要先执行父类的构造函数:因为继承关系中,子类是依附于父类的,没有父类就不会有子类,因此要先执行父类的构造函数。

当一个类里有另一个类的对象时(我们暂时称这个对象为子对象),构造函数的调用顺序为:

  1. 调用父类的构造函数,对父类成员初始化

  2. 调用子对象的构造函数,对子对象成员初始化

  3. 调用子类的构造函数,对子类成员初始化

可以这样想:如果子对象都没被实例化,那子类的成员就没有被确定,自然要先确定好子对象,再来调用子类的构造函数。


C++多态与虚函数

多态与虚函数

定义:“一个接口,多种方法”,程序在运行时才决定调用的函数。

实现:C++多态性主要是通过虚函数实现的,虚函数允许在子类里重写override。

目的:接口重用。封装可以使得代码模块化,继承可以扩展已存在的代码,它们的目的都是为了代码重用。而多态的目的是为了接口重用。

用法:声明基类的指针,利用该指针指向任意一个子类对象,调用相应的虚函数,可以根据指向的子类的不同类型而实现不同的方法。

C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
下面实例中,基类Shape被派生为两个类:

class Shape
{
protected:
	int width, height;
public:
	Shape(int a, int b)
	{
		width = a;
		height = b;
	}
	int area()
	{
		cout << "Parent class area :" << endl;
		return 0;
	}
};
class Rectangle :public Shape
{
public:
	Rectangle(int a = 0, int b = 0) :Shape(a, b) {}
	int area()
	{
		cout << "Rectangle class area :" << endl;
		return (width * height);
	}
};
class Triangle :public Shape
{
public:
	Triangle(int a = 0, int b = 0) :Shape(a, b) {}
	int area()
	{
		cout << "Triangle class area :" << endl;
		return (width * height / 2);
	}
};

int main()
{
	Shape* shape;
	Rectangle rec(10, 7);
	Triangle tri(10, 5);

	//存储矩形的地址
	shape = &rec;
	//调用矩形的求面积函数 area
	shape->area();

	//存储三角形的地址
	shape = &tri;
	//调用三角形的求面积函数 area
	shape->area();

	return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Parent class area
Parent class area

导致错误输出的原因是,调用函数area()被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链接——函数调用在程序执行前就准备好了。有时候也被称为早绑定,因为area()函数在程序编译期间就已经设置好了。

现在,我们对程序稍作修改,在Shape类中,area()的声明前放置关键字virtual:

class Shape
{
protected:
	int width, height;
public:
	Shape(int a, int b)
	{
		width = a;
		height = b;
	}
	virtual int area()
	{
		cout << "Parent class area :" << endl;
		return 0;
	}
};

修改后,当编译和执行前面的实例代码时,它会产生以下结果:

Rectangle class area
Triangle class area

此时,编译器看的是指针的内容,而不是指针的类型。因此,由于tri和rec类的对象的地址存储在*shape中,所以会调用各自的area()函数。正如我们所看到的,每个子类都有一个函数area()的独立实现。这就是多态的一般使用方式。有了多态,可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。这种操作被称为动态链接,或后期绑定

注意:父类中的虚函数和子类中对虚函数的重写必须要有相同的函数名、参数列表和返回值。构造函数不能是虚函数,析构函数可以是虚函数。其实,基类的析构函数最好写成virtual,否则在子类对象销毁的时候,无法销毁子类对象部分资源。

纯虚函数

我们可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是我们在基类中不能对虚函数给出有意义的实现,这时候就会用到纯虚函数。
我们可以把基类中的虚函数area()改写如下:

class Shape
{
protected:
	int width, height;
public:
	Shape(int a, int b)
	{
		width = a;
		height = b;
	}
	//纯虚函数
	virtual int area() = 0;
};

纯虚函数后面的=0代替了之前的函数体,上面的虚函数就是纯虚函数。

虚函数使用技巧:private的虚函数
考虑下面的例子:

class A
{
public:
	void foo() { bar(); }
private:
	virtual void bar() {/*……*/ };
};

class B :public A
{
private:
	virtual void bar() {/*……*/ };
};

在这个例子中,虽然bar()在A类中是private的,但是仍然可以出现在派生类中,并仍然可以与public或者protected的虚函数一样产生多态的效果。并不会因为它是private的,就发生A::foo()不能访问B::bar()的情况,也不会发生B::bar()对A::bar()的override不起作用的情况。
这种写法的意思是:A告诉B,你最好override我的bar()函数,但是你不要管它如何使用,也不要自己调用这个函数。


虚函数表

多态是由虚函数实现的,而虚函数主要是通过虚函数表(V-Table)来实现的。
如果一个类中包含虚函数,那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。如下图:

这个类的每一个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),这个虚指针指向虚函数表。

注:对象不包含虚函数表,只有虚指针,派生类会生成一个兼容基类的虚函数表。

原始基类的虚函数表
下图是原始基类的对象,可以看到虚指针在地址的最前面,指向基类的虚函数表(假设基类定义了3个虚函数)

单继承时的虚函数(无重写基类虚函数)
假设现在派生类继承基类,并且重新定义了3个虚函数,派生类会自己产生一个兼容基类虚函数表的属于自己的虚函数表

Derive class继承了Base class中的三个虚函数,准确地说,是该函数实体的地址被拷贝到Derive类的虚函数表,派生类新增的虚函数置于虚函数表的后面,并按声明顺序存放

单继承时的虚函数(重写基类虚函数)
现在派生类重写基类的x函数,可以看到这个派生类构建自己的虚函数表的时候,修改了base:()这一项,指向了自己的虚函数。

多重继承时的虚函数(Derived::public Base1,public Base2)
这个派生类多重继承了两个基类base1,base2,因此它有两个虚函数表。

它的对象会有多个虚指针(据说和编译器相关),指向不同的虚函数表。

虚继承时的虚函数表
虚继承的引入把对象的模型变得十分复杂,除了每个基类(MyClassA和MyClassB)和公共基类(MyClass)的虚函数表需要记录外,每个虚拟继承了MyClass的父类还需要记录一个虚基类表vbTable的指针vbPtr。
MyClass的对象模型如图所示。

虚基类每项记录了被继承的虚基类子对象相对于虚基类表指针的偏移量。比如MyClass的虚基类表第二项记录值为24,正是MyClass::vfptr相对于MyClass::vbptr的偏移量,同理MyClassB的虚基类表第二项记录值12也正是MyClass::vfptr相对于MyClass::vbptr的偏移量。


类Class的大小

一个类new出来的对象的size有几个字节?加上一个虚函数呢?(虚继承后呢?)

理论结论:

  • 非静态成员变量总和
  • 加上编译器为了CPU计算,做出的数据对齐处理
  • 加上为了支持虚函数,产生的额外负担

实验:

1.空类的size

class Test
{

};

int main()
{
	int size = 0;
	Test test;
	size = sizeof(test);
	cout << size << endl;
}

运行结果:1

编译器在执行Test test;这行代码后需要,作出一个Test test的Object。并且这个Object的地址还是独一无二的,于是编译器就会给空类创建一个隐含的一个字节的空间。

2 只有成员变量的size

class Test
{
	int a;
	int b;
};

int main()
{
	int size = 0;
	Test test;
	size = sizeof(test);
	cout << size << endl;
}

运行结果:8

在32位中,一个整型变量占4字节。

3 加入一个静态成员变量

class Test
{
	int a;
	int b;
	static int c;
};

运行结果:8

和结论中第一条:非静态成员变量的总和。

4 数据对齐处理的情况

class Test
{
	char d;
	int a;
	int b;
	static int c;
};

加入一个char字符型变量
运行结果:12

编译器额外添加3个字符变量,做了数据对齐处理,为了提高CPU的运算速度。
至于为什么会提高CPU的效率,与CPU寻址相关,对齐后寻址更方便。

5 只有成员函数的size

class Test
{
public:
	Test() {};
	~Test() {};
	void fun() {};
};

运行结果:1

函数不占用空间的。

6 有虚函数的情况

class Test
{
public:
	Test() {};
	~Test() {};
	virtual void fun() {};
};

运行结果:4

是指向virtual table的虚指针vptr的size
虚函数的实现主要靠两个东西,虚函数指针和虚函数表。
一个类中所有的虚函数通过一个虚函数指针管理。
虚函数表由各个类之间共享,里面存放了虚函数的地址。
虚函数指针指向表中对应的虚函数地址。

普通成员函数不占类对象的大小空间,因为普通成员函数通过this指针管理,一个对象的this指针并不是对象本身的一部分,不会影响sizeof的结果。


overload、override和overwrite

虚函数总是在派生类中被改写,这种改写被称为override。
override(重写)是指派生类重写基类的虚函数,重写的函数必须有一致的参数列表和返回值。有时候会用“覆盖”和“改写”来指代。

overload(重载)约定俗成地被翻译为“重载”。是指编写一个与已有函数同名但是参数列表不同的函数。例如一个函数既可以接收整型作为参数,也可以接收浮点型作为参数。overload对函数的返回值没有设置要求,但不能存在只有返回值不同的重载函数。

overwrite(隐藏):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:1、如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。2、如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。


C++多重继承

多重继承,即一个派生类可以有两个或多个基类。

class D::public A,private B,protected C{
   //类D新增加的成员
}

多继承容易让代码逻辑复杂、思路混乱,一直饱受争议,中小型项目中较少使用,后来的Java、C#、PHP等干脆取消了多继承。

多继承下的构造函数

多继承形势下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。
以上面的A、B、C、D类为例,D类构造函数的写法为:
D(形参列表):A(实参列表),B(实参列表),C(实参列表){
   //其他操作
}

基类构造函数的调用顺序和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。即使将D类构造函数写作斜面的形式:
D(形参列表):B(实参列表),C(实参列表),A(实参列表){
   //其他操作
}
那么也是先调用A类的构造函数,再调用B类的构造函数,最后调用C类的构造函数

命名冲突

当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析附“::”,以显式地指明到底使用哪个类的成员,消除二义性。


用new和不用new创建对象的区别

  • new创建类对象需要指针接收,一处初始化,多处使用
  • new创建对象使用完需要delete销毁
  • new创建对象直接使用堆空间,而局部不用new定义类对象则使用栈空间
  • new对象指针用途广泛,比如作为函数返回值、函数参数等
  • 频繁调用场合不适合new,就像new申请和delete释放内存一样

new创建类对象的例子

CTest *pTest = new CTest();
delete pTest;
pTest用来接收类对象指针。
不用new,直接使用类定义申明:CTest mTest;
这种创建方式,使用完后不需要手动释放,该类析构函数会自动执行。而new申请的对象,则只有调用到delete时才会执行析构函数,如果程序退出而没有执行delete则会造成内存泄漏。

只定义类指针

CTest *pTest = NULL;
这跟不用new申明对象有很大区别,类指针可以先行定义,但类指针只是个通用指针,在new之前并不会对类对象分配任何内存空间。
但使用普通方式创建的类对象,在创建之初就已经分配了内存空间。而类指针,如果未经过对象初始化,则不需要delete释放。

new对象指针作为函数参数和返回值

class CTest { public: int a; };
class CBest { public: int b; };
CTest* fun(CBest* pBest)
{
	CTest* pTest = new CTest();
	pTest->a = pBest->b;
	return pTest;
}

int main()
{
	CBest *pBest = new CBest();
	CTest* pRes = fun(pBest);
	if (pBest != NULL)
		delete pBest;
	if (pRes != NULL)
		delete pRes;
	return 0;
}

new和malloc的区别

总结:

  • malloc和new都是在堆上开辟内存的
    malloc只负责开辟内存,没有初始化功能,需要用户自己初始化;new不但开辟内存,还可以进行初始化,如new int(10);表示在堆上开辟了一个4字节的int整型内存,初始值是10,再如new int[10]();表示在堆上开辟了一个包含10个整型元素的数组,初始值都为0。

  • malloc是函数,开辟内存需要传入字节数,如malloc(100);表示在堆上开辟了100个字节的内存,返回void*,表示分配的堆内存的起始地址,因此malloc的返回值需要强转成指定类型的地址;new是运算符,开辟内存需要指定类型。返回指定类型的地址,因此不需要进行强转。

详细对比:

属性:
new和delete是C++关键字,效率高于malloc和free,但需要编译器支持;malloc和free是库函数,需要头文件支持。

参数:
使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

返回类型:
new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void*,需要通过强制类型转换将void*指针转换成我们需要的类型。

自定义类型:
new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数,只能动态地申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

重载:
C++允许重载new/delete操作符,malloc不允许重载。

分配失败:
new内存分配失败时,会抛出bad_alloc异常。malloc分配内存失败时返回NULL。

内存泄漏:
内存泄漏new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc却不可以。

用free来释放new出来的对象会有什么问题?
free函数只会释放内存,而delete在释放内存之前还多一步执行对象析构函数的步骤,因此用free函数释放new出来的对象,会造成内存泄漏的问题。

如堆上开辟int整型:

//根据传入字节数开辟内存,没有初始化
int* p1 = (int*)malloc(sizeof(int));

//根据指定类型int开辟一块整型内存,初始化为0
int* p2 = new int(0);

//开辟400个字节的内存,相当于包含100个整型元素的数组,没有初始化
int* p3 = (int*)malloc(sizeof(int) * 100);

//开辟400个字节的内存,100个元素的整型数组,元素都初始化为0
int* p4 = new int[100]();

new/new[]用法

//开辟单地址空间
//开辟大小为sizeof(int)空间
int *p = new int;
//开辟大小为sizeof(int)的空间,并初始化为5
int* q = new int(5);

//开辟数组空间
//一维
//大小为100的整型数组空间,并初始化为0
int *a = new int[100]{ 0 };

//二维
int(*a)[6] = new int[5][6];

//三维
int(*a)[5][6] = new int[3][5][6];

//四维及以上以此类推

delete/delete[]用法

//释放单个int空间
int* a = new int;
delete a;

//释放int数组空间
int* b = new int[5];
delete []b;

结构体和共同体的区别

定义:
结构体struct:把不同类型的数据组合成一个整体,自定义类型。
共同体union:使几个不同类型的变量共同占用一段内存。

地址:
struct和union都有内存对齐,结构体的内存布局依赖于CPU、操作系统、编译器及编译时的对其选项。

内存对齐

关于内存对齐,先让我们看四个重要的基本概念:

  • 数据类型自身的对齐值:
    对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
  • 结构体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
  • 指定对齐值:#pragme pack(n),n=1,2,4,8,16改变系统的对齐系数。
  • 数据成员、结构体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。

首先根据结构体内部成员的自身对齐值得到结构体的自身对齐值(内部成员最大的长度),如果没有修改系统设定的默认补齐长度的话,取较小的进行内存补齐。

结构体struct:不同之处,struct里每个成员都有自己独立的地址。sizeof(struct)是内存对齐后所有成员长度的加和。
共同体union:当共同体中存入新的数据后,原有的成员就失去了作用,新的数据被写道union的地址中。sizeof(union)是最长的数据成员的长度。

总结:struct和union都是由多个不同的数据类型成员组成,但在任何同一时刻,union中只存放了一个被选中的成员,而struct的所有成员都存在。在struct中,各成员都占有自己的内存空间,它们是同时存在的。一个struct变量的总长度等于所有成员长度之和。在union中,所有成员不能同时占用它的内存空间,它们不能同时存在。union变量的长度等于最长的成员的长度。对于union的不同成员赋值,将会对其他成员重写,原来成员的值就不存在了,而对于struct的不同成员赋值是互不影响的。


static和const分别怎么用

static和const分别怎么用,类里面static和const可以同时修饰成员函数吗?

static的作用:

对变量:

  • 局部变量:
    在局部变量之前加上关键字static,局部变量就被定义成为一个局部静态变量。
    内存中的位置:静态存储区
    初始化:局部的静态变量只能被初始化一次,且C中不可以用变量对其初始化,而C++可以用变量对其初始化。
    作用域:作用域仍为局部作用域,当定义它的函数或者语句块结束的时候,作用域随之结束。
    注:当static用来修饰局部变量的时候,它就改变了局部变量的存储位置(从原来的栈中存放改为静态存储区)及其生命周期(局部静态变量在离开作用域之后,并没有被销毁,而是仍然驻留在内存当中,直到程序结束,只不过我们不能再对它进行访问),但未改变其作用域。
  • 全局变量:
    在全局变量之前加上关键字static,全局变量就被定义成为一个全局静态变量。
    内存中的位置:静态存储区(静态存储区在整个程序运行期间都存在)
    初始化:未经初始化的全局静态变量会被程序自动初始化为0(自动对象的值是任意的,除非它被显式初始化)。
    作用域:全局静态变量在声明它的文件之外是不可见的。准确地将从定义之处开始到文件结尾。如果要在其他文件中用到该变量,需要在其他文件中用extern声明
    注:static修饰全局变量,并未改变其存储位置及生命周期,而是改变了其作用域,使当前文件外的源文件无法访问该变量,好处如下:1、不会被其他文件访问,修改 2、其他文件中可以使用相同名字的变量,不会发生冲突。对全局函数也是有隐藏作用。而普通全局变量只要定义了,任何地方都能使用,使用前需要声明所有的.c文件,只
    能定义一次普通全局变量,但是可以声明多次(外部链接)。注意:全局变量的作用域是全局范围,但是在某个文件中使用时,必须先声明。

对类中的:

  • 成员变量:
    用static修饰类的数据成员实际使其成为类的全局变量,会被类的所有对象共享,包括派生类的对象。因此,static成员必须在类外进行初始化(初始化格式: int base::var=10;),而不能在构造函数内进行初始化,不过也可以用const修饰static数据成员在类内初始化。
    特点:
    • 1 不要试图在头文件中定义(初始化)静态数据成员。在大多数的情况下,这样做会引起重复定义这样的错误。即使加上#ifndef #define #endif 或者#pragma once也不行。
    • 2 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
    • 3 静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为所属类类型的指针或引用。
  • 成员函数:
    • 1 用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针。
    • 2 静态成员函数是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。base::func(); 当static成员函数在类外定义时不需要加static修饰符。
    • 3 在静态成员函数的实现中不能直接引用类中说明的非静态成员,可以引用类中说明的静态成员。因为静态成员函数不含this指针。

不可以同时用const和static修饰成员函数。
C++编译器在实现const的成员函数的时候为了确保该函数不能修改类的实例的状态,会在函数中添加一个隐式的参数const this*。但当一个成员为static的时候,该函数是没有this指针的。也就是说此时const的用法和static是冲突的。
我们也可以这样理解:两者的语意是矛盾的。static的作用是表示该函数只作用在类的静态变量上,与类的实例没有关系;而const的作用是确保函数不能修改类的实例的状态,与类的静态变量没有关系。因此不能同时用它们。

const的作用:

  • 限定变量为不可修改。在定义该const变量时,需初始化,以后就没有机会改变它了。
  • 限定成员函数不可以修改任何数据成员。(如果要在const函数里修改数据成员,则需要在数据成员前加 mutalbe 关键字修饰)
  • const与指针:
    • const char *p表示:指向的内容不能改变。
    • char * const p,就是将p声明为常指针,它的地址不能改变,是固定的,但是它的内容可以改变。

指针和引用的区别,引用可以用常指针实现吗

本质上的区别是,指针是一个新的变量,只是这个变量存储的是另一个变量的地址,我们通过访问这个地址来修改变量。
而引用只是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行操作,因此以达到修改变量的目的。

具体:

  • 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:
    int a=1; int *p=&a;
    int a=1; int &b=a;
    上面定义了一个整型变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
    下面定义了一个整型变量a和这个整型a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
  • 可以有const指针,const char *p(常量指针)表示p指向的内容不可更改,char* const p(指针常量)表示该指针存储的地址不可更改,指向的内容可以变。(const引用可读不可改,与绑定是否为const无关)
  • 指针可以有多级,但是引用只能是一级(int **p;合法 而int &&a是不合法的)
  • 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化。
  • 指针的值在初始化后可以改变,即指向其他的存储单元,而引用在进行初始化后就不会再改变了。
  • “sizeof”引用得到的是所指向的变量(对象)的大小,而”sizeof”指针得到的是指针本身的大小。
  • 指针和引用的自增“++”运算以意是不一样的。

指针传参的时候,还是值传递,试图修改传进来的指针的值是不可以的。只能修改地址所保存变量的值(加上解引用符*)。
引用传参的时候,传进来的就是变量本身,因此可以被修改。


深拷贝与浅拷贝

浅拷贝与深拷贝的概念来自于当用对象A去初始化另一个对象B时(还有赋值,对象值传递函数参数,对象值传递函数返回),之所以会成功,是因为编译器为我们自动添加了类的默认拷贝构造函数,而默认拷贝构造函数仅仅使用“老对象”的数据成员的值对“新对象”的数据成员一一进行赋值,它一般具有以下形式:

Rect::Rect(const Rect& r)
{
width = r.width;
height = r.height;
}

补充:拷贝构造函数是一种特殊的构造函数,函数的名称必须和类名称一致,它必须的一个参数是本类型的一个引用变量。

浅拷贝:

所谓浅拷贝,指的是在对象复制时,只对对象中的数据成员进行简单地赋值,默认拷贝构造函数执行的也是浅拷贝。大多情况下“浅拷贝”已经能很好地工作了,但是一旦对象存在了动态成员,那么浅拷贝就会出现问题,让我们考虑如下一段代码:

class Rect
{
public:
    Rect()      // 构造函数,p指向堆中分配的一空间
    {
        p = new int(100);
    }
    ~Rect()     // 析构函数,释放动态分配的空间
    {
        if (p != NULL)
        {
            delete p;
        }
    }
private:
    int width;
    int height;
    int* p;     // 一指针成员
};

int main()
{
    Rect rect1;
    Rect rect2(rect1);   // 复制对象
    return 0;
}

在这段代码允许结束前,会出现一个允许错误。原因就在于进行对象复制时,对于动态分配的内容没有进行正确的操作。我们来分析一下:
在允许定义rect1对象后,由于在构造函数中有一个动态分配的语句,因此执行后的内存情况如下:

在使用rect1赋值rect2时,由于执行的是浅拷贝,只是将成员的值进行赋值,这时rect.p = rect2.p,也就是这两个指针指向了堆里的同一个空间,如下图所示:

当然,这不是我们所期望的结果,在销毁对象时,两个对象的析构函数将对同一个内存空间释放两次,这就是错误出现的原因。我们需要的不是两个p有相同的值,而是两个p指向的空间有相同的值,解决办法就是使用“深拷贝”。

深拷贝:

在“深拷贝”的情况下,对于对象中动态成员,就不能仅仅简单地赋值了,而应该重写动态分配空间,如上面的例子就应该按照如下的方式进行处理:

class Rect
{
public:
    Rect()      // 构造函数,p指向堆中分配的一空间
    {
        p = new int(100);
    }
    Rect(const Rect& r)
    {
        width = r.width;
        height = r.height;
        p = new int;    // 为新对象重新动态分配空间
        *p = *(r.p);
    }
    ~Rect()     // 析构函数,释放动态分配的空间
    {
        if (p != NULL)
        {
            delete p;
        }
    }
private:
    int width;
    int height;
    int* p;     // 一指针成员
};

此时,在完成对象的复制后,内存的一个大致情况如下:

此时rect1的p和rect2的p各自指向一段内存空间,但它们指向的空间具有相同的内容,这就是所谓的“深拷贝”。

防止默认拷贝发生:

通过对对象复制的分析,我们发现对象的复制大多在进行“值传递”时发生,这里有一个小技巧可以防止按值传递——声明一个私有拷贝构造函数。甚至不必去定义这个拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按照值传递或函数返回该类对象,将得到一个编译错误,从而可以避免按值传递或返回对象。

private:
    //拷贝构造,只是声明
    CExample(const CExample& C);
int main()
{
    CExample test(1);
    //g_Fun(test);按值传递将出错

    return 0;
}

拷贝构造函数的几个细节:

  1. 拷贝构造函数里能调用private成员变量吗?
    可以。拷贝构造函数其实就是一个特殊的构造函数,操作的还是自己类的成员变量,所以不受private的限制。
  2. 以下函数哪个是拷贝构造函数,为什么?
    X::X(const X&);
    X::X(X);
    X::X(X&, int a=1);

    X::X(X&, int a=1, int b=2);
    对于一个类X,如果一个构造函数的第一个参数是下列之一:
    X&
    const X&
    volatile X&
    const volatile X&
    且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。
    X::X(const X&);  //是拷贝构造函数
    X::X(X&, int a=1);  //是拷贝构造函数
    X::X(X&, int a=1, int b=2);  //也是拷贝构造函数
  3. 一个类中可以存在多于一个的拷贝构造函数吗?
    可以。
class X
{
public:
    X(const X&);  //const的拷贝构造
    X(X&);  //非const的拷贝构造
};

printf和cout的区别

  • 原理不同
  • cout与printf格式不同
  • 输出效率不同

原理:

std::cout<<”输出内容”std::endl;
其中<<操作符提取“输出内容”,然后进行重载,同时重载函数,根据“输出内容”的类型来重载不同类型的函数。
同时在定义每一个流对象时,系统会在内存中开辟一段缓冲区,用来暂存数据(系统有多个缓冲区)。此时当收到endl时,cout会进行换行,同时刷新缓冲区。
当缓冲区满或者收到结束符时,会将缓冲区数据一并清空并输出到显示设备。

格式:

cout: std::cout<<”任意类型函数”std::endl;
printf: printf(“其他+%转换+其他”,参数);

转换说明输出
%a浮点数、十六进制数字和p-记数法(C99)
%A浮点数、十六进制数字和P-记数法(C99)
%c一个字符
%d有符号十进制整数
%e浮点数、e-记数法
%E浮点数、E-记数法
%f浮点数、十进制记数法
%g根据数值不同自动选择%f或者%e。%e格式在指数小于-4或者大于等于精度时使用
%G根据数值不同自动选择%f或者%E。%E格式在指数小于-4或者大于等于精度时使用
%i有符号十进制整数(与%d相同)
%o无符号八进制整数
%p指针(就是指地址)
%s字符串
%u无符号十进制整数
%x使用十六进制数字0f的无符号十六进制整数
%X使用实录紧张数字0F无符号十六进制整数
%%打印一个百分号

效率:

cin cout效率没有scanf printf高
流输入输出优势:

  • 流输入输出对于基本类型来说使用很方便,不用手写格式控制字符串。
  • 对于标准库的一些class来说,显然重载操作符也比自己写格式控制字符串要方便很多。
  • 对于复杂的格式可以进行自定义操作符。
  • 可读性更好。

其实原理上来说流操作的效率比printf/scanf函数族更高,因为是在编译期确定操作数类型和调用的输出函数,不用在运行期解析格式控制字符串带来额外开销。不过标准流对象cin/cout为了普适性,继承体系很复杂,所以在对象的构造等方面会影响效率,因此总体效率比较低。如果根据特定的场景进行优化,效率可以更高一点。

最后总结,C++的iostream库和C中的stdio库中分别的cout/cin和printf/scanf相比有哪些优势呢?首先是类型处理更加安全,更加智能,我们无须应对int、float中的%d、%f,而且扩展性极强,对于新定义的类,printf想要输入输出一个自定义的类成员是天方夜谭的,而iostream中使用的位运算符都是可重载的,并且可以将清空缓冲区的自由交给用户(在printf中输出是没有缓冲区的),而且流风格的写法也更加自然和简洁。


智能指针

智能指针的作用

C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄漏(忘记释放),二次释放,程序发生异常时内存泄漏等问题。使用智能指针能更好地管理堆内存。

理解智能指针需要从下面三个层次:

  • 从较浅的层面看,智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装。这使得智能指针实质是一个对象,行为表现得却像一个指针。
  • 智能指针的作用是防止忘记调用delete释放内存和程序异常地进入catch块忘记释放内存。另外指针的释放时机也是非常有考究的,多次释放同一个指针会造成程序崩溃,这些都可以通过智能指针来解决。
  • 智能指针还有一个作用是把值语义转换成引用语义。C++和Java有一处最大的区别在于语义不同,在Java里面下列代码:
    Animal a=new Animal();
    Animal b=a;
    这里其实只生成了一个对象,a和b仅仅是把持对象的引用而已。但在C++中不是这样,
    Animal a;
    Animal b=a;
    这里却是生成了两个对象。

智能指针的使用

智能指针在C++11版本之后提供,包含在头文件<memory>中,shared_ptr,unique_ptr,wear_ptr。

shared_ptr的使用

shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用它一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。

  • 初始化:智能指针是一个模板类,可以指定类型,传入指针通过构造函数初始化。也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,一个是类,一个是指针。例如std::shared_ptr<int> p=new int(1);的写法是错误的。
  • 拷贝和赋值:拷贝使得对象的引用计数增加1,赋值使得原对象引用计数减1,当计数为0时,自动释放内存。后来指向的对象引用计数加1,指向后来的对象。
  • get函数获取原始指针。
  • 注意不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。
  • 注意避免循环引用,shared_ptr的一个最大的陷阱是循环引用,循环引用会导致堆内存无法正确释放,导致内存泄漏。
#include<memory>
#include<iostream>
using namespace std;

int main()
{
    int a = 10;
    shared_ptr<int> ptra = make_shared<int>(a);
    shared_ptr<int> ptra2(ptra);  //copy
    cout << ptra.use_count() << endl;

    int b = 20;
    int* pb = &a;
    //shared_ptr<int> ptrb=pb;  //error
    shared_ptr<int> ptrb = make_shared<int>(b);
    ptra2 = ptrb;  //assign
    pb = ptrb.get();  //获取原始指针

    cout << ptra.use_count() << endl;
    cout << ptrb.use_count() << endl;
}

unique_ptr的使用

unique_ptr“唯一“拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义,只有移动语义来实现)。相比于原始指针,unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其它操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定,通过reset方法重新指定,通过release方法释放所有权,通过移动语义转移所有权。

int main()
{
    {
        unique_ptr<int> uptr(new int(10)); //绑定动态对象
        //unique_ptr<int> uptr2 = uptr; //不能赋值
        //unique_ptr<int> uptr2(uptr);  //不能拷贝
        unique_ptr<int> uptr2 = move(uptr);  //转换所有权
        uptr2.release();  //释放所有权
    }
    //超过uptr的作用域,内存释放
}

weak_ptr的使用

weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用shared_ptr对象,从而操作资源。但当expired()==true,lock()函数返回一个存储空指针的shared_ptr。

int main()
{
    {
        shared_ptr<int> sh_ptr = make_shared<int>(10);
        cout << sh_ptr.use_count << endl;

        weak_ptr<int> wp(sh_ptr);
        cout << wp.use_count() << endl;

        if (!wp.expired())
        {
            shared_ptr<int> sh_ptr2 = wp.lock();  //get another shared_ptr
            *sh_ptr = 100;
            cout << wp.use_count() << endl;
        }
    }
    //delete memory
}

智能指针的设计和实现

下面是一个简单智能指针的demo。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作用为另一对象的副本而创建时,拷贝构造函数拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)。智能指针就是模拟指针动作的类。所有的智能指针都会重载->和*操作符。智能指针还有许多其他功能,比较有用的是自动销毁。这主要是利用栈对象的有限作用域以及临时对象(有限作用域实现)析构函数释放内存。

#include<memory>
#include<iostream>
using namespace std;

template<typename T>
class SmartPointer
{
private:
    T* _ptr;
    size_t* _count;
public:
    SmartPointer(T* ptr = nullptr) :_ptr(ptr)
    {
        if (_ptr)
        {
            _count = new size_t(1);
        }
        else
        {
            _count = new size_t(0);
        }
    }

    SmartPointer(const SmartPointer &ptr)
    {
        if (this != &ptr)
        {
            this->_ptr = ptr._ptr;
            this->_count = ptr._count;
            (*this->_count)++;
        }
    }

    SmartPointer& operator=(const SmartPointer& ptr)
    {
        if (this->_ptr == ptr._ptr)
        {
            return *this;
        }
        if (this->_ptr)
        {
            (*this->_count)--;
            if (this->_count == 0)
            {
                delete this->_ptr;
                delete this->_count;
            }
        }

        this->_ptr = ptr._ptr;
        this->_count = ptr._count;
        (*this->_count)++;
        return *this;
    }

    T& operator*()
    {
        assert(this->_ptr == nullptr);
        return *(this->_ptr);
    }

    T* operator->()
    {
        assert(this->_ptr == nullptr);
        return this->_ptr;
    }

    ~SmartPointer()
    {
        (*this->_count)--;
        if (*this->_count == 0)
        {
            delete this->_ptr;
            delete this->_count;
        }
    }

    size_t use_count()
    {
        return *this->_count;
    }
};

int main()
{
    {
        SmartPointer<int> sp(new int(10));
        SmartPointer<int> sp2(sp);
        SmartPointer<int> sp3(new int(20));
        sp2 = sp3;
        cout << sp.use_count() << endl;
        cout << sp3.use_count() << endl;
    }
    //delete operator
}

struct和class的区别

在C++中我们可以看到struct和class的区别并不是很大,两者之间有很大的相似性。那么为什么还要保留struct,这时因为C++是向下兼容的,因此C++中保留了很多C的东西。

首先来看一下C中的struct

struct A
{
    int a;
    int b;
    //成员列表
};

因为struct是一种数据类型,那么就肯定不能定义函数,所以在面向C的过程中,struct不能包含任何函数。否则编译器会报错。
面向过程的编程认为,数据和数据操作是分开的。然而当struct进入面向对象的C++时,其特性也有了新发展,就拿上面的错误函数来说,在C++中就能运行,因为C++中认为数据和数据对象是一个整体,不应该分开,这就是struct在C和C++两个时代的差别。

在C++中struct得到了很大的扩充:

  • struct可以包括成员函数
  • struct可以实现继承
  • struct可以实现多态

区别

  • 默认的继承访问权。class默认的是private,struct默认的是public。(当class默认继承struct时,是私有继承,反之是公有继承)
  • 默认访问权限。struct是public,而class是private。
    从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。
  • class和struct在使用大括号{}上的区别。

关于使用大括号初始化:

  • class和struct如果定义了构造函数的话,都不能使用大括号进行初始化。
  • 如果没有定义构造函数,struct可以用大括号初始化
  • 如果没有定义构造函数,且所有成员变量全是public的话,class可以用大括号初始化
下一篇