面经——C C++常见面试知识点总结附面试真题_c++中级面试

static int m_int; //static成员变量
};

int example::m_int = 0; //没有static

cout<<example::m_int; //可以直接通过类名调用静态成员变量

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8


#### 静态成员函数


* 1). 静态成员函数是类所共享的;
* 2). 静态成员函数可以访问静态成员变量,但是不能直接访问普通成员变量(需要通过对象来访问);需要注意的是普通成员函数既可以访问普通成员变量,也可以访问静态成员变量;
* 3). 可以通过对象名直接访问公有静态成员函数;
* 4). 可以通过类名直接调用公有静态成员函数,即不需要通过对象,这一点是普通成员函数所不具备的。



class example{
private:
static int m_int_s; //static成员变量
int m_int;
static int getI() //静态成员函数在普通成员函数前加static即可
{
return m_int_s; //如果返回m_int则报错,但是可以return d.m_int是合法的
}
};

cout<<example::getI(); //可以直接通过类名调用静态成员变量

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9
* 10
* 11


### 10. 在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?


C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同,假设某个函数原型为:



      void foo(int x, int y);
* 1


该函数被C编译器编译后在库中的名字为 \_foo, 而C++编译器则会产生像: \_foo\_int\_int 之类的名字。为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。


### 11. 头文件中的 ifndef/define/endif 是干什么用的? 该用法和 program once 的区别?


相同点:  
 它们的作用是防止头文件被重复包含。  
 不同点


* 1). ifndef 由语言本身提供支持,但是 program once 一般由编译器提供支持,也就是说,有可能出现编译器不支持的情况(主要是比较老的编译器)。
* 2). 通常运行速度上 ifndef 一般慢于 program once,特别是在大型项目上, 区别会比较明显,所以越来越多的编译器开始支持 program once。
* 3). ifndef 作用于某一段被包含(define 和 endif 之间)的代码, 而 program once 则是针对包含该语句的文件, 这也是为什么 program once 速度更快的原因。
* 4). 如果用 ifndef 包含某一段宏定义,当这个宏名字出现“撞车”时,可能会出现这个宏在程序中提示宏未定义的情况(在编写大型程序时特性需要注意,因为有很多程序员在同时写代码)。相反由于program once 针对整个文件, 因此它不存在宏名字“撞车”的情况, 但是如果某个头文件被多次拷贝,program once 无法保证不被多次包含,因为program once 是从物理上判断是不是同一个头文件,而不是从内容上。


### 12. 当i是一个整数的时候++i和i++那个更快一点?i++和++i的区别是什么?


答:理论上++i更快,实际与编译器优化有关,通常几乎无差别。



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

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

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9
* 10
* 11
* 12
* 13
* 14


i++和++i的考点比较多,简单来说,就是i++返回的是i的值,而++i返回的是i+1的值。也就是++i是一个确定的值,是一个可修改的左值,如下使用:



cout << ++(++(++i)) << endl;
cout << ++ ++i << endl;

* 1
* 2


可以不停的嵌套++i。  
 这里有很多的经典笔试题,一起来观摩下:



int main()
{
int i = 1;
printf(“%d,%d\n”, ++i, ++i); //3,3
printf(“%d,%d\n”, ++i, i++); //5,3
printf(“%d,%d\n”, i++, i++); //6,5
printf(“%d,%d\n”, i++, ++i); //8,9
system(“pause”);
return 0;
}

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9
* 10


首先是函数的参数入栈顺序从右向左入栈的,计算顺序也是从右往左计算的,不过都是计算完以后再进行的压栈操作:  
 对于第1个printf,首先执行++i,返回值是i,这时i的值是2,再次执行++i,返回值是i,得到i=3,将i压入栈中,此时i为3,也就是压入3,3;  
 对于第2个printf,首先执行i++,返回值是原来的i,也就是3,再执行++i,返回值是i,依次将3,5压入栈中得到输出结果  
 对于第3个printf,首先执行i++,返回值是5,再执行i++返回值是6,依次将5,6压入栈中得到输出结果  
 对于第4个printf,首先执行++i,返回i,此时i为8,再执行i++,返回值是8,此时i为9,依次将i,8也就是9,8压入栈中,得到输出结果。  
 上面的分析也是基于VS搞的,不过准确来说函数多个参数的计算顺序是未定义的(the order of evaluation of function arguments are undefined)。笔试题目的运行结果随不同的编译器而异。


### 1. C中static有什么作用


  (1)隐藏。 当我们同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性,故使用static在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。


  (2)static的第二个作用是保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量。


  (3)static的第三个作用是默认初始化为0.其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0×00,某些时候这一特点可以减少程序员的工作量。


### 2.C++中const有什么用?


  不要一听到const就说是常量,这样给考官一种在和一个外行交谈的感觉。应该说const修饰的内容不可改变就行了, 定义常量只是一种使用方式而已,还有const数据成员,const参数, const返回值, const成员函数等, 被const修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。


### 3. C与C++各自是如何定义常量的?有什么不同?


  C中是使用宏#define定义, C++使用更好的const来定义。


  区别:


  1)const是有数据类型的常量,而宏常量没有,编译器可以对前者进行静态类型安全检查,对后者仅是字符替换,没有类型安全检查,而且在字符替换时可能会产生意料不到的错误(边际效应)。


  2)有些编译器可以对const常量进行调试, 不能对宏调试。


### 4. 既然C++中有更好的const为什么还要使用宏?


  const无法代替宏作为卫哨来防止文件的重复包含。


### 5. C++中引用和指针的区别?


  引用是对象的别名, 操作引用就是操作这个对象, 必须在创建的同时有效得初始化(引用一个有效的对象, 不可为NULL), 初始化完毕就再也不可改变, 引用具有指针的效率, 又具有变量使用的方便性和直观性, 在语言层面上引用和对象的用法一样, 在二进制层面上引用一般都是通过指针来实现的, 只是编译器帮我们完成了转换。 之所以使用引用是为了用适当的工具做恰如其分的事, 体现了最小特权原则。


### 6. 说一说C与C++的内存分配方式?


  1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,如全局变量,static变量。


  2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。


  3)从堆上分配(动态内存分配)程序在运行的时候用malloc或new申请任意多少的内存,程序员负责在何时用free或delete释放内存。动态内存的生存期自己决定,使用非常灵活。


### 7. new/delete 与 malloc()/free() 的区别?


  malloc() 与 free() 是C语言的标准库函数, new/delete 是C++的运算符, 他们都可以用来申请和释放内存, malloc()和free()不在编译器控制权限之内, 不能把构造函数和析构函数的任务强加给他们。www.cdtarena.com


### 8. #include<a.h>和#include"a.h" 有什么区别?


  答:对于#include <a.h> ,编译器从标准库路径开始搜索 a.h对于#include "a.h" ,编译器从用户的工作路径开始搜索 a.h


### 9. 在C++ 程序中调用被 C编译器编译后的函数,为什么要加 extern “C”?


  C++语言支持函数重载,C语言不支持函数重载。函数被C++编译后在库中的名字与C语言的不同。假设某个函数的原型为: void foo(int x, int y);该函数被C编译器编译后在库中的名字为\_foo,而C++编译器则会产生像\_foo\_int\_int之类的名字。C++提供了C连接交换指定符号extern"C"来解决名字匹配问题。


### 10. C++中的什么是多态性? 是如何实现的?


  多态性是面向对象程序设计语言继数据抽象和继承之后的第三个基本特征。它是在运行时出现的多态性通过派生类和虚函数实现。基类和派生类中使用同样的函数名, 完成不同的操作具体实现相隔离的另一类接口,即把" w h a t"从"h o w"分离开来。多态性提高了代码的组织性和可读性,虚函数则根据类型的不同来进行不同的隔离。


### 11. 什么是动态特性?


  在绝大多数情况下, 程序的功能是在编译的时候就确定下来的, 我们称之为静态特性。 反之, 如果程序的功能是在运行时刻才能确定下来的, 则称之为动态特性。C++中, 虚函数,抽象基类, 动态绑定和多态构成了出色的动态特性。


### 12.什么是封装?C++中是如何实现的?


  封装来源于信息隐藏的设计理念, 是通过特性和行为的组合来创建新数据类型让接口与具体实现相隔离。C++中是通过类来实现的, 为了尽量避免某个模块的行为干扰同一系统中的其它模块,应该让模块仅仅公开必须让外界知道的接口。


### 13. 什么是RTTI?


  RTTI事指运行时类型识别(Run-time type identification)在只有一个指向基类的指针或引用时确定一个对象的准确类型。


### 14. 什么是拷贝构造函数?


  它是单个参数的构造函数,其参数是与它同属一类的对象的(常)引用;类定义中,如果未提供自己的拷贝构造函数,C++提供一个默认拷贝构造函数,该默认拷贝构造函数完成一个成员到一个成员的拷贝


### 15. 什么是深浅拷贝?


  浅拷贝是创建了一个对象用一个现成的对象初始化它的时候只是复制了成员(简单赋值)而没有拷贝分配给成员的资源(如给其指针变量成员分配了动态内存); 深拷贝是当一个对象创建时,如果分配了资源,就需要定义自己的拷贝构造函数,使之不但拷贝成员也拷贝分配给它的资源。


### 16.面向对象程序设计的优点?


  开发时间短, 效率高, 可靠性高。面向对象编程的编码具有高可重用性,可以在应用程序中大量采用成熟的类库(如STL),从而虽短了开发时间,软件易于维护和升级。


## 第三部分:数组、指针 & 引用


### 1. 指针和引用的区别?


相同点:


* 1). 都是地址的概念;
* 2). 都是“指向”一块内存。指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名;
* 3). 引用在内部实现其实是借助指针来实现的,一些场合下引用可以替代指针,比如作为函数形参。

 不同点: 
* 1). 指针是一个实体,而引用(看起来,这点很重要)仅是个别名;
* 2). 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
* 3). 引用不能为空,指针可以为空;
* 4). “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小;
* 5). 指针和引用的自增(++)运算意义不一样;
* 6). 引用是类型安全的,而指针不是 (引用比指针多了类型检查)
* 7). 引用具有更好的可读性和实用性。


### 2. 引用占用内存空间吗?


如下代码中对引用取地址,其实是取的引用所对应的内存空间的地址。这个现象让人觉得引用好像并非一个实体。但是引用是占用内存空间的,而且其占用的内存和指针一样,因为引用的内部实现就是通过指针来完成的。  
   
 比如 Type& name; <===> Type\* const name。



int main(void)
{
int a = 8;
const int &b = a;
int *p = &a;
*p = 0;
cout<<a; //output 0
return 0;
}

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9


### 3. 三目运算符


在C中三目运算符(? :)的结果仅仅可以作为右值,比如如下的做法在C编译器下是会报错的,但是C++中却是可以是通过的。这个进步就是通过引用来实现的,因为下面的三目运算符的返回结果是一个引用,然后对引用进行赋值是允许的。



int main(void)
{
int a = 8;
int b = 6;
(a>b ? a : b) = 88;
cout<<a; //output 88
return 0;
}

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8


### 4. 指针数组和数组指针的区别


数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。


* 数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (\*p)[10],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。



类型名 (*数组标识符)[数组长度]

* 1


* 指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如 int \*p[n], []优先级高,先与p结合成为一个数组,再由int\*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 \*p=a; 这里\*p表示指针数组第一个元素的值,a的首地址的值。



类型名 *数组标识符[数组长度]

* 1


### 5. 左值引用与右值引用


该部分主要摘自:[c++ 学习笔记]( )


左值引用就是我们通常所说的引用,如下所示。左值引用通常可以看作是变量的别名。



type-id & cast-expression

// demo
int a = 10
int &b = a

int &c = 10 // 错误,无所对一个立即数做引用

const int &d = 10 // 正确, 常引用引用常数量是ok的,其等价于 const int temp = 10; const int &d = temp

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9


右值引用是 C++11 新增的特性,其形式如下所示。右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。



type-id && cast-expression


// demo  
 int &&var = 10; // ok


int a = 10  
 int &&b = a // 错误, a 为左值


int &&c = var // 错误,var 为左值


int &&d = move(a) // ok, 通过move得到左值的右值引用



* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9
* 10
* 11


在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。


### 6. 右值引用的意义


* 右值引用支持移动语义的实现,可以减少拷贝,提升程序的执行效率。

 下面的代码时没有采用右值引用时的实现。



class Stack
{
public:
// 构造
Stack(int size = 1000)
:msize(size), mtop(0)
{
cout << “Stack(int)” << endl;
mpstack = new int[size];
}

// 析构
~Stack()
{
cout << "~Stack()" << endl;
delete[]mpstack;
mpstack = nullptr;
}

// 拷贝构造
Stack(const Stack &src)
:msize(src.msize), mtop(src.mtop)
{
cout << "Stack(const Stack&)" << endl;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
    mpstack[i] = src.mpstack[i];
}
}

// 赋值重载
Stack& operator=(const Stack &src)
{
cout << "operator=" << endl;
if (this == &src)
 	    return *this;

delete[]mpstack;

msize = src.msize;
mtop = src.mtop;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
    mpstack[i] = src.mpstack[i];
}
return *this;
}

int getSize() 
{
return msize;
}

private:
int *mpstack;
int mtop;
int msize;
};

Stack GetStack(Stack &stack)
{
Stack tmp(stack.getSize());
return tmp;
}

int main()
{
Stack s;
s = GetStack(s);
return 0;
}

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9
* 10
* 11
* 12
* 13
* 14
* 15
* 16
* 17
* 18
* 19
* 20
* 21
* 22
* 23
* 24
* 25
* 26
* 27
* 28
* 29
* 30
* 31
* 32
* 33
* 34
* 35
* 36
* 37
* 38
* 39
* 40
* 41
* 42
* 43
* 44
* 45
* 46
* 47
* 48
* 49
* 50
* 51
* 52
* 53
* 54
* 55
* 56
* 57
* 58
* 59
* 60
* 61
* 62
* 63
* 64
* 65
* 66
* 67
* 68
* 69
* 70


运行结果如下。



Stack(int) // 构造s
Stack(int) // 构造tmp
Stack(const Stack&) // tmp拷贝构造main函数栈帧上的临时对象
~Stack() // tmp析构
operator= // 临时对象赋值给s
~Stack() // 临时对象析构
~Stack() // s析构

* 1
* 2
* 3
* 4
* 5
* 6
* 7


执行代码的过程中调用拷贝构造,将内存中的内容逐个拷贝,在 C++ 11 中可以借助右值引用实现移动拷贝构造和移动赋值来解决这个问题。



Stack(Stack &&src)
:msize(src.msize), mtop(src.mtop)
{
cout << “Stack(Stack&&)” << endl;

/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;  
src.mpstack = nullptr;

}

// 带右值引用参数的赋值运算符重载函数
Stack& operator=(Stack &&src)
{
cout << “operator=(Stack&&)” << endl;

if(this == &src)
    return *this;
    
delete[]mpstack;

msize = src.msize;
mtop = src.mtop;

/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;

return *this;

}

* 1
* 2
* 3
* 4
* 5
* 6
* 7
* 8
* 9
* 10
* 11
* 12
* 13
* 14
* 15
* 16
* 17
* 18
* 19
* 20
* 21
* 22
* 23
* 24
* 25
* 26
* 27
* 28
* 29


执行结果如下。可以看到,在有拷贝构造和移动拷贝构造函数的时候,优先调用了移动拷贝构造和移动赋值。在移动拷贝构造和移动赋值中直接把资源所有权进行了转移,而非拷贝,这就大大提高了执行效率。



Stack(int) // 构造s
Stack(int) // 构造tmp
Stack(Stack&&) // 调用带右值引用的拷贝构造函数,直接将tmp的资源给临时对象
~Stack() // tmp析构
operator=(Stack&&) // 调用带右值引用的赋值运算符重载函数,直接将临时对象资源给s
~Stack() // 临时对象析构
~Stack() // s析构

* 1
* 2
* 3
* 4
* 5
* 6
* 7


* 右值引用在可以使重载函数变得更加简洁。右值引用可以适用 const T& 和 T& 形式的参数。
* ```
struct W  
{  
   W(int&, int&) {}  
};  
  
struct X  
{  
   X(const int&, int&) {}  
};  
  
struct Y  
{  
   Y(int&, const int&) {}  
};  
  
struct Z  
{  
   Z(const int&, const int&) {}  
};


template <typename T, typename A1, typename A2>  
T* factory(A1& a1, A2& a2)  
{  
   return new T(a1, a2);  
} 


template <typename T, typename A1, typename A2>  
T* factory_new(A1&& a1, A2&& a2)  
{  
   return new T(std::forward<A1>(a1), std::forward<A2>(a2));  
}  

// demo
int a = 2;
int b = 2;

W* c = factory<w>(a, b);	// ok
Z* d = factory<Z>(2, 2);	// 错误,2 是右值

W* pw = factory_new<W>(a, b);	// ok
X* px = factory_new<X>(2, b);	// ok
Y* py = factory_new<Y>(a, 2);	// ok
Z* e = factory_new<Z>(2, 2);	// ok
W* f = factory_new<W>(2, 2);	// 错误, 

 
 
 
 
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46更多相关内容可以参考:[c++——左值、右值、左值引用、右值引用]( )

第四部分:C++特性

1. 什么是面向对象(OOP)?面向对象的意义?

Object Oriented Programming, 面向对象是一种对现实世界理解和抽象的方法、思想,通过将需求要素转化为对象进行问题处理的一种思想。其核心思想是数据抽象、继承和动态绑定(多态)。
面向对象的意义在于:将日常生活中习惯的思维方式引入程序设计中;将需求中的概念直观的映射到解决方案中;以模块为中心构建可复用的软件系统;提高软件产品的可维护性和可扩展性。

2. 解释下封装、继承和多态?

+ 1). **封装**:  
 封装是实现面向对象程序设计的第一步,封装就是将数据或函数等集合在一个个的单元中(我们称之为类)。  
 封装的意义在于保护或者防止代码(数据)被我们无意中破坏。  
 从封装的角度看,public, private 和 protected 属性的特点如下。


	- 不管那种属性,内类都是可以访问的
	- public 是一种暴露的手段,比如暴露接口,类的对象可以访问
	- private 是一种隐藏的手段,类的对象不能访问
	- protected 成员:
		* 和 public 一样可以被子类继承
		* 和 private 一样不能在类外被直接调用
		* 特例:在衍生类中可以通过衍生类对象访问,如下代码所示
class Base  
{  
public:  
    Base(){};  
    virtual ~Base(){};  
protected:  
    int int_pro;  
};
class A : public Base  
{  
public:  
    A(){};  
    A(int da){int_pro = da;}  
    // 通过 obj 对象直接访问 protected 成员
    void Set(A &obj){obj.int_pro = 24;}	
    void PrintPro(){cout << "The proteted data is " << int_pro <<endl;}  
};  

 
 
 
 
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 2). **继承**:  
 继承主要实现重用代码,节省开发时间。  
 子类可以继承父类的一些东西。


	- a.\*\*公有继承(public)\*\*公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态(基类的私有成员仍然是私有的,不能被这个派生类的子类所访问)。
	- b.\*\*私有继承(private)\*\*私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员(并且不能被这个派生类的子类所访问)。
	- c.\*\*保护继承(protected)\*\*保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员(并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的)。这里特别提一下虚继承。虚继承是解决C++多重继承问题(其一,浪费存储空间;第二,存在二义性问题)的一种手段。比如菱形继承,典型的应用就是 iostream, 其继承于 istream 和 ostream,而 istream 和 ostream 又继承于 ios。3).**多态**:  

多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。与之相对应的编译时绑定函数称为静态绑定。多态是设计模式的基础,多态是框架的基础。

3. 什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?

+ 1). 没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数。
+ 2). 深拷贝是指拷贝后对象的逻辑状态相同,而浅拷贝是指拷贝后对象的物理状态相同;默认拷贝构造函数属于浅拷贝。
+ 3). 当系统中有成员指代了系统中的资源时,需要深拷贝。比如指向了动态内存空间,打开了外存中的文件或者使用了系统中的网络接口等。如果不进行深拷贝,比如动态内存空间,可能会出现多次被释放的问题。是否需要定义拷贝构造函数的原则是,是类是否有成员调用了系统资源,如果定义拷贝构造函数,一定是定义深拷贝,否则没有意义。  
 更多可以参考下面的代码,比较容易混淆的是赋值操作符,其实区分很简单,在出现等号的时候,如果有构造新的对象时调用的就是构造,不然就是赋值操作符。
class A {
public:
	A() {
		m = new int[4]{ 1,2,3,4 };
		std::cout << "constructor" << std::endl;
	}
	~A() {
		if (m != nullptr) {
			delete[] m;
		}
	}
	A(const A& a) {
		this->m = new int[4];
		memcpy(a.m, this->m, this->len * sizeof(int));
		std::cout << "copy constructor" << std::endl;
	}
	// 移动构造
    A(A&& a) : m(a.m) {
		a.m = nullptr; 
		std::cout << "move constructor" << std::endl;
	}
    // 赋值操作符重载
    A& operator= (const A& a) {
        memcpy(a.m, this->m, this->len * sizeof(int));
        std::cout << "operator" << std::endl;
        return *this;
    }
​
private:
    int len = 4;
    int* m = nullptr;
};
​
A getA(A a) {
    return a;
}
​
int main(void)
{
    A a;    // construct
    
    A b = a;    // copy construct
    A c(a); // copy construct
    
    A d;    // construct
    d = a;  // operate
​
    A e = getA(a);  // construct, move construct
​
    return 0;
}

 
 
 
 
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26
+ 27
+ 28
+ 29
+ 30
+ 31
+ 32
+ 33
+ 34
+ 35
+ 36
+ 37
+ 38
+ 39
+ 40
+ 41
+ 42
+ 43
+ 44
+ 45
+ 46
+ 47
+ 48
+ 49
+ 50
+ 51### 4. 构造函数和析构函数的执行顺序?
构造函数
+ 1). 首先调用父类的构造函数;
+ 2). 调用成员变量的构造函数;
+ 3). 调用类自身的构造函数。#### 析构函数

对于栈对象或者全局对象,调用顺序与构造函数的调用顺序刚好相反,也即后构造的先析构。对于堆对象,析构顺序与delete的顺序相关。

5. 虚析构函数的作用?

基类采用虚析构函数可以防止内存泄漏。比如下面的代码中,如果基类 A 中不是虚析构函数,则 B 的析构函数不会被调用,因此会造成内存泄漏。

class A{
public:
  A(){}
  //~A(){}
  virtual ~A(){}  // 虚析构
};
class B : public A{
public:
  B(){
    // new memory
  }
  ~B(){
    // delete memory
  }
};
int main(int argc, char *argv)
{
  A *p = new B;
  
  // some operations
  // ...
  
  delete p;  // 由于基类中是虚析构,这里会先调用B的析构函数,然后调用A的析构函数
  
  return 0;
}

 
 
 
 
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+ 11
+ 12
+ 13
+ 14
+ 15
+ 16
+ 17
+ 18
+ 19
+ 20
+ 21
+ 22
+ 23
+ 24
+ 25
+ 26**但并不是要把所有类的析构函数都写成虚函数**。因为当类里面有虚函数的时候,编译器会给类添加一个**虚函数表**,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来**作为基类的时候**,才把析构函数写成虚函数。

6. 细看拷贝构造函数

对于 class A,它的拷贝构造函数如下:

 A::A(const A &a){}

 
 
 
 
+ 1##### 1) 为什么必须是当前类的引用呢?

循环调用。如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

2) 为什么是 const 引用呢?

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年嵌入式&物联网开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新!!

的析构函数都写成虚函数**。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

6. 细看拷贝构造函数

对于 class A,它的拷贝构造函数如下:

 A::A(const A &a){}

 
 
 
 
+ 1##### 1) 为什么必须是当前类的引用呢?

循环调用。如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。

只有当参数是当前类的引用时,才不会导致再次调用拷贝构造函数,这不仅是逻辑上的要求,也是 C++ 语法的要求。

2) 为什么是 const 引用呢?

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

因此收集整理了一份《2024年嵌入式&物联网开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-6R6EkDBG-1715841694490)]

[外链图片转存中…(img-wYIeNTnv-1715841694490)]

[外链图片转存中…(img-OwN6GrnS-1715841694491)]

[外链图片转存中…(img-wvjjQnGt-1715841694491)]

[外链图片转存中…(img-tVGhs3dX-1715841694492)]

[外链图片转存中…(img-87P4VI0h-1715841694492)]

[外链图片转存中…(img-6sZGXawb-1715841694493)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上嵌入式&物联网开发知识点,真正体系化!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新!!

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值