面试C++总结

总结C++面试知识点,以及在刷题过程中发现的点。

1. 静态变量static的作用

C/C++程序经过编译链接之后形成的二进制映像文件,这些文件包含:堆,栈,数据段(也叫静态存储区)和代码段。其中堆和栈为动态区域,数据段和代码段为静态区域

  1. 栈区stack:由编译器自动分配释放,存放函数的参数值,局部变量等值。随着作用域而进栈出栈。最大占空间默认是1M,不过可以调整。
  2. 堆区heap:允许程序在运行时动态申请某个大小的内存。注意这个区域一般由程序员来维护(malloc和free)。如果不进行释放,很可能会造成内存泄露。
  3. 数据段:由三部分组成。只读数据段是程序中使用的一些不会被更改的数据,一般由const修饰的变量,以及程序中使用的文字常量。已初始化的读写数据段,已初始化数据是在程序中声明,并且具有初值的变量,主要为已初始化的全局变量和已初始化的静态局部变量(static修饰)。未初始化段存放程序中未初始化的全局变量和静态变量。
  4. 代码段:存放函数体的二进制代码,所有语句编译后会生成CPU指令存储在代码区。

new/delete和malloc/free的区别
new/delete 是c++的关键字,而malloc/free是c语言的库函数。后者的使用必须指明申请内存空间的大小,对于class类型的对象,或者不会调用构造函数和析构函数。前者会调用构造函数,不用指定内存大小,返回的指针不用强转。

所以由static修饰的变量会存放在静态存储区,在整个程序中一直存在。

  • 全局变量,静态全局变量在声明他的文件之外是不可见的,准确的说就是从定义之处开始到文件结尾。但运行过程中一直占用内存。
  • 局部变量,作用域为局部作用域,当定义它的函数或者语句块结束的时候,作用域结束。一样,当结束作用域的时候并没有销毁,只是不能进行访问。
  • 静态函数。函数的定义和声明在默认情况下都是extern的。但静态函数只在声明它的文件中可见,不能被其他文件使用,不会与其他文件中的同名函数引起冲突。
  • 类的静态成员。在类中,静态成员可以实现多个对象之间的数据共享。注意类的静态成员,不是对象成员,多个对象中的数据成员只存储一个,供所有对象使用。引用不需要用对象名。
  • 类的静态函数。与类的静态数据成员一样,不是对象成员。在静态成员函数的实现中不能直接引用类中说明的非静态成员,但可以引用类中说明的静态成员。因为非静态成员要通过对象来引用。

注意,在刷题过程中发现,STL中的sort函数,如果要使用自定义cmp函数,这个cmp函数需要是静态的或者是全局的。如果是自己写main函数,可以在main函数之外定义全局cmp函数,如果是在class中写成员函数,就需要加static修饰cmp函数(也就是类的静态函数)。

2. C++中指针和引用的区别

引用变量是一个别名,也就是说,它是某个已经存在变量的另一个名字。它必须连接到一块合法的内存。这也就是为什么引用必须在创建时初始化。

1
2
int i = 17;
int& r = i; //这里r是一个初始化为i的整数引用。当改变r的值的时候,同时会改变i的值。
  1. 指针本身占有一个4个字节的空间,而引用只是一个别名。使用sizeof看一个指针,返回4;看一个引用,返回的是被引用对象的大小。
  2. 指针可以被初始化为NULL,而引用必须初始化而且必须是一个已有对象的引用(比如0)。
  3. 作为参数传递的时候,指针需要被解引用(*操作)才能对对象进行操作;直接对引用的修改都会改变引用所指对象。
  4. 可以用const指针,但没有const引用。
  5. 指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变。
  6. 指针可以有多级指针(**p),而引用最多一级。
  7. 指针和引用使用++运算符的意义不一样,指针++是指向下一个内存地址。引用则是在该内存表示的变量上加1。
  8. 如果返回动态内存分配的对象或内存,必须使用指针,引用可能引起内存泄露。但在函数中可以通过引用来动态改变动态内存中的值,函数声明为void。即void function(int& a){}

3. const关键字

const在C++中用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数。const允许指定一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某个值是保持不变的。相对应的volatile关键字跟const相反,是易变的,不会被编译器优化。

  1. const修饰普通类型变量

    1
    2
    3
    const int a = 8;
    int b = a; //这个操作是拷贝。
    a = 7; //错误,不能改变
  2. const修饰指针

1
2
3
4
5
6
7
8
9
10
//A. const修饰指针指向的内容,则内容为不可变量。
const int *p = 8; //指针指向的内容8不可变
//B. const修饰指针,指针为不可变量,也就是说指针的地址不可变,但是地址中存储的值可以变。这个指针无法进行++操作。
int a = 8;
int* const p = &a;
*p = 9; //正确
int b = 7;
p = &b; //错误
// C. const 修饰指针和指针所指向的内容,则指针和指针指向的内容都不可变。
const int * const p = &a;
  1. const 参数传递和函数返回值。

参数传递:
A. 函数值传递,一般不需要const修饰,因为函数会自动产生临时变量复制实参数。

1
2
3
4
void function(const int a){
cout << a;
//++a; 是错误的因为a不能被改变;
}

B. 当const参数为指针时,可以防止指针被意外篡改。
C. 自定义类型的参数传递,采用const加引用。这样防止对引用参数进行更改。
函数返回值:
A. const修饰内置类型的返回值,修饰和不修饰返回值作用一样
B. const修饰自定义类型作为返回值,返回的值不能作为左值使用,既不能被赋值,也不能被修改

  1. const修饰类成员函数

const修饰类成员函数,目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,那么成员函数都应当声明为const成员函数。 void function() const

常量定义:常量在c++里定义就是一个top-level const加上对象类型,常量定义必须初始化。对于局部对象,常量存放在栈区;对于全局对象,常量存放在全局、静态存储区。对于字面值常量,常量存放在常量存储区。注意与static区分,static修饰的变量都存放在静态存储区。

4. C++中smart pointer的四个智能指针:shared_ptr, unique_ptr, weak_ptr, auto_ptr

智能指针的作用是管理一个指针,因为存在以下情况:申请的空间在函数结束时忘记释放,造成内存泄露。智能指针本身是一个类,当超出了类的作用域时,类会自动调用析构函数,释放资源。

  1. auto_ptr (C++98的方案,cpp11已经抛弃) 采用所有权模式。
    1
    2
    3
    4
    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.”));
    auto_ptr<string> p2;
    p2 = p1; //auto_ptr不会报错.
    //但是当程序访问p1将会报错,所以缺点是存在潜在的内存奔溃问题。
  2. unique_ptr(替换auto_ptr)
    实现独占拥有概念,保证同一时间内只有一个只能指针可以指向该对象。
  3. shared_ptr
    实现共享概念,多个智能指针可以指向相同对象,该对象和相关资源会在”最后一个引用被销毁“时释放。通过成员函数use_count()来查看资源的所有者个数。
  4. weak_ptr
    是一种不控制对象声明周期的智能指针,它指向一个shared_ptr管理的对象,weak_ptr只提供了对管理对象的一个访问手段。weak_ptr对象的构造,他的构造和析构不会引起引用计数的增加或减少。

智能指针有没有内存泄露的情况
当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄露。
为了解决循环引用导致的内存泄露,引入了weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但不指向引用计数的共享内存。

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
class B; //类声明
class A{
public:
shared_ptr<B> pb_; //循环引用1
~A()
{
cout<<"A delete\n";
}
};
class B{
public:
shared_ptr<A> pa_; //循环引用2
~B()
{
cout<<"B delete\n";
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main(){
fun();
return 0;
}

运行fun()到use_count()的时候,每个类的引用次数都为2。出了作用域后,指针引用减1,两个指针的引用次数都不会为0。但如果我们吧class A中的shared_ptr改为weak_ptr,这样的话pb.use_count()只会有1,出了作用域之后,pb引用次数降为0,得到释放,B的释放也会导致A的计数减1,同时A得到释放。

注意我们不能通过weak_ptr直接访问对象的方法。需要通过weak_ptr.lock()先转化为shared_ptr再进行访问。

5. 虚函数

C++类的实质就是struct,struct与class的区别就在于class允许了继承和虚函数。也就是说struct缺少的就是虚函数。

  1. 定义一个函数为虚函数virtual并不代表函数不被实现。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  2. 定义一个函数为纯虚函数virtual void function()=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
class A  
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<<"B::foo() is called"<<endl;
}
};
int main(void)
{
A *a = new B(); //基类指针指向子类
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}

作者:wuxinliulei
链接:https://www.zhihu.com/question/23971699/answer/69592611
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

6. 重载和重写 (c++的多态)

重载(静态多态):两个函数名相同,但是参数列表不同,返回值类型没有要求,在同一作用域中。本质上是两个函数。
构成函数重载要满足以下几个条件:1. 作用域相同;2.函数名相同;3. 参数列表不同。(参数个数,参数类型或参数顺序不同,包括返回参数)
重写(动态多态):子类继承了父类,父类中的函数是虚函数,在子类中重新定义了这个虚函数。这种情况是重写。

静态函数和虚函数的区别:静态函数在编译时就已经确定了运行时机,虚函数在运行的时候动态绑定。虚函数因为使用了虚函数表机制,调用的时候会增加一次内存开销,降低效率。

7. STL容器

容器 底层实现 功能支持
vector 数组,一倍数组长度增加,内存中是连续的存储 支持快速随机访问
list 双向链表,在内存中并不是连续存储 只是快速增删
deque 双向队列, 支持首尾增删,也支持随机访问
stack 底层一般用list或deque实现
queue 底层一般用list或deque实现
set 红黑树,有序 不重复
map 红黑树,有序 不重复
hash_set hash表,无序 不重复
hash_map hash表,无序 不重复
  1. map和set有什么区别:
  • map和set都是c++的关联容器,底层实现都是红黑树。都能够实现自动排序。
  • map元素是key-value对,set就是关键字的简单集合。set中每个元素只包含一个关键字。
  • set的迭代器是const的,不允许修改元素的值;map允许修改value,但是不允许修改key。原因就是map和set是根据关键字排序来保证其有序性。
  • map支持下标操作,set不支持下标操作。不过,如果find能解决需要,尽可能用find。
  1. STL迭代器删除元素:
  • 对于序列容器vector,queue来说,使用erase(iterator)之后,后边的每个元素的迭代器都会失效,但是后边每个元素都会向前移动一个位置,但是erase会返回一个有效的迭代器;
  • 对于关联容器map,set来说,使用erase后,当前元素的迭代器失效,删除当前元素不会影响到下一个元素的迭代器。
  • 对于list,他使用了不连续分配的内存,并且他的erase方法也会返回下一个有效的iterator。
  1. vector和list的区别:
  • vector:连续存储的容器,动态数组,在堆上分配连续内存空间。底层实现为数组。数组容量两倍长度增长。访问O(1),尾部插入删除很快,中间插入删除需要进行数组拷贝。使用场景:经常随机访问,且不经常对非尾节点进行插入删除。
  • list:动态链表,在堆上分配不连续空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间。插入删除很快,常数开销。使用场景:经常插入删除大量数据。

8. 基本类型与存储大小

基本类型 存储大小(32位)/字节 存储大小(64位)/字节 取值范围(32位)
char 1 1 -128 ~ 127
unsigned char (当byte用) 1 1 0 ~ 255
short 2 2 -32768 ~ 32767
unsigned short 2 2 0~65536
int 4 4 -2,147,483,648 ~ 2,147,483,647
unsigned int 4 4 0 ~ 4,294,967,295
long 4 8 –2,147,483,648 ~ 2,147,483,647
unsigned long 4 8 0 ~ 4,294,967,295
long long 8 8 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
指针 4 8
float 4 4 3.4E +/- 38 (7 digits)
double 8 8 1.7E +/- 308 (15 digits)

注意浮点型数字不能使用==来判断是否相等,事实上上应该尽量避免判断浮点数是否相等,因为精度的问题,一般不能得到正确的结论,如果非要判断浮点数是否相等,可以判断两数的差是否小到可以忽略即可。