C++面试必备知识归纳

这篇文章也许会持续更新,也欢迎大家提出问题,一起探讨。原文地址AC4Fun,转载请注明出处。

****************分割线************************************
按照侯捷先生在《Effective C++》的观点,以及自己的一些理解,可以将互联网技术岗位关于C++的知识点归纳为以下五个部分:

  • C++基础知识
  • 面向过程的特性
  • 面向对象的特性
  • 泛型编程的特性
  • 标准模板库和算法

这是我第一次在简书写文章,如果大家有什么意见的话,欢迎随时向我提出来。必须要说明的是,C++知识繁杂磅礴,面试题中可被问到的很多,如果想要成功拿到心仪的offer,除了掌握C++这个工具外,需要结合一些其它领域知识,如基础数据结构和基本的算法,网络编程,多线程编程,(Linux)Shell脚本(awk, sed),编译器使用(gcc/clang, makefile, automake/cmake等),数据库(SQL语言,数据库理论如存储引擎,索引实现,范式等)视频处理,设计模式,机器学习等知识,如果后续有时间,也会对相关知识进行一下归纳。
本文主要侧重于纪录分析C++面试中的基础知识,主要包括:关键字使用,

1. 关键字

auto

C++98以前,auto关键字是继承B语言而来,C++11中已被废弃,新auto主要用于自动类型推断和返回值占位

static

static修饰的变量和函数存储在内存的静态文本区,与全局变量存储的位置一致。而静态文本区的字节默认都是0x00

  • 修饰局部变量
    对局部变量添加static修饰符后,变量的存储区由栈改为静态文本区,它的生存周期得到了改变。
  • 修饰全局变量
    对全局变量添加static,生存周期不会改变,但是会影响作用域。普通全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误。这一点,有人称作“隐藏”
  • 修饰函数
    static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static修饰的函数),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件.
    static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝

const

const关键字可以修饰变量,引用,函数,对象等:
常变量: const 类型说明符 变量名
常引用: const 类型说明符 &引用名
常对象: 类名 const 对象名
常成员函数: 类名::fun(形参) const
常数组: 类型说明符 const 数组名[大小]
常指针: const 类型说明符* 指针名 ,类型说明符* const 指针名

在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。

需要注意的概念其实是“常量指针”“指针常量”,也就是const修饰一个指针变量的时候产生的两种差异。我们知道,一个指针变量,使用的时候需要考虑该指针本身和被它所指的对象,看如下例子:

char *const pc; //到char的const指针
char const *pc1; //到const char的指针
const char *pc2; //到const char的指针(后两个声明是等同的)

从右向左读的记忆方式:
pc is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容。pc是一个指向字符类型的常指针,pc的值不可变,但是pc值(也就是pc指向的地址)所代表的内存空间的内容是可以变的,所以pc是一个指针常量(const pointer)。
pc2 is a pointer to const char. 故pc2的内容不可以改变,但pc2可以指向别的字符串。也就是说pc2是指向一个不可变内容空间(常量)的指针,也就是常量指针(pointer to const)。pc2++可行,但pc2 = "hello world"不可行。当然,只是说不能通过pc2去修改那段内容,别的方式是可以的。

其实,const只对它左边的东西起作用,唯一的例外就是const本身就是最左边的修饰符,那么它才会对右边的东西起作用。
理解之后,再来看下面两个,就很容易明白了。

vector<string>::const_iterator iter  //iter is a pointer to const string
//iter值可变,但是*iter不可重新被赋值,常量指针

const vector<string>::iterator cit  //cit is a const pointer to string
//cit值不可变,但是*cit可被重新赋值,指针常量

常量函数 常量函数是C++对常量的一个扩展,它很好的确保了C++中类的封装性。在C++中,为了防止类的数据成员被非法访问,将类的成员函数分成了两类,一类是常量成员函数(也被称为观察者);另一类是非常量成员函数(也被成为变异者)。在一个函数的签名后面加上关键字const后该函数就成了常量函数。对于常量函数,最关键的不同是编译器不允许其修改类的数据成员。
当然,我们可以绕过编译器的错误去修改类的数据成员。但是C++也允许我们在数据成员的定义前面加上mutable,以允许该成员可以在常量函数中被修改。当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。

另外,需要注意C++中用const定义了一个常量后,不会分配一个空间给它,而是将其写入符号表(symbol table),这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。只有当使用extern或者取地址操作的时候,才会分配空间,但是这不会影响到常量本身的值,因为用到a的时候,编译器根本不会去进行内存空间的读取。这就是c++的常量折叠(constant folding),即将const常量放在符号表中,而并不给其分配内存。编译器直接进行替换优化。其值仍旧从符号表中读取,不管常量对应的存储空间中的值如何变化,都不会对其值产生影响。

宏定义与const

C++中定义常量的时候不再采用define,因为define只做简单的宏替换,并不提供类型检查.

四种类型转换

  • static_cast
    static_cast 很像 C 语言中的旧式类型转换。它能进行基础类型之间的转换,也能将带有可被单参调用的构造函数或用户自定义类型转换操作符的类型转换,还能在存有继承关系的类之间进行转换(即可将基类转换为子类,也可将子类转换为基类),还能将 non-const对象转换为 const对象(注意:反之则不行,那是const_cast的职责。)。
    注意:static_cast 转换时并不进行运行时安全检查,所以是非安全的,很容易出问题。因此 C++ 引入 dynamic_cast 来处理安全转型。

  • dynamic_cast
    dynamic_cast 主要用来在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。如果转型失败会返回null(转型对象为指针时)或抛出异常(转型对象为引用时)。dynamic_cast 会动用运行时信息(RTTI)来进行类型安全检查,因此 dynamic_cast 存在一定的效率损失。

class CBase { };
 class CDerived: public CBase { };
 CBase b;
 CBase* pb;
 CDerived d;
 CDerived* pd;
 pb = dynamic_cast<CBase*>(&d);     // ok: derived-to-base
 pd = dynamic_cast<CDerived*>(&b);  // error: base-to-derived

上面的代码中最后一行 VS2010 会报如下错误:
error C2683: 'dynamic_cast' : 'CBase' is not a polymorphic typeIntelliSense: the operand of a runtime dynamic_cast must have a polymorphic class type
这是因为 dynamic_cast 只有在基类带有虚函数的情况下才允许将基类转换为子类。当然,允许转换也不代表可以转换成功。

class CBase
 {
    virtual void dummy() {}
 };
 class CDerived: public CBase
 {
     int a;
 };

  int main ()
 {
    CBase * pba = new CDerived;
    CBase * pbb = new CBase;
    CDerived * pd1, * pd2;
    pd1 = dynamic_cast<CDerived*>(pba);
    pd2 = dynamic_cast<CDerived*>(pbb);
   return 0;
 }

结果是:上面代码中的 pd1 不为 null,而 pd2 为 null。
dynamic_cast 也可在 null 指针和指向其他类型的指针之间进行转换,也可以将指向类型的指针转换为 void 指针(基于此,我们可以获取一个对象的内存起始地址 *const void * rawAddress = dynamic_cast<const void > (this);)。

  • const_cast
    前面提到 const_cast 可去除对象的常量性(const),它还可以去除对象的易变性(volatile)。const_cast 的唯一职责就在于此,若将 const_cast 用于其他转型将会报错。
  • reinterpret_cast
    reinterpret_cast 用来执行低级转型,如将执行一个 int 的指针强转为 int。其转换结果与编译平台息息相关,不具有可移植性,因此在一般的代码中不常见到它。reinterpret_cast 常用的一个用途是转换函数指针类型,即可以将一种类型的函数指针转换为另一种类型的函数指针,但这种转换可能会导致不正确的结果。总之,reinterpret_cast 只用于底层代码,一般我们都用不到它,如果你的代码中使用到这种转型,务必明白自己在干什么。

总结来看,需要类型转换的时候,优先使用C++的这种风格进行类型转换,基础类型转换的时候,使用static_cast, 子类与父类之间进行转换的时候,尤其是基类向子类转换的时候,使用dynamic_cast。其它情况,如转换为void指针,使用dynamic_cast, int型指针到int,以及函数指针之间的转换使用reinterpret_cast, const_cast只用于去除对象的常量性(const)和易变性(volatile)
抛开C++为了兼容C而允许隐式类型转换(隐蔽,不安全,易引起非预期的函数调用,对象切割等等),我倾向于认为C++是一种强类型(倾向于不允许隐式类型转换),静态类型(编译前已经知道数据类型)的语言。

重载与重写

  • 重载(Overload)
    同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。成员函数被重载的特征:
    (1)相同的范围(在同一个类中);
    (2)函数名字相同;
    (3)参数不同;
    (4)virtual关键字可有可无。

  • 重写(Override),也叫覆盖(Overwrite)
    重写是指派生类函数重写基类函数,是C++的多态的表现,特征是:
    (1)不同的范围(分别位于派生类与基类);
    (2)函数名字相同;
    (3)参数相同;
    (4)基类函数必须有virtual关键字。

说到底,这两个概念其实并没有太大的关联,重载是编译多态的一种实现,重写与虚函数相关,用于实现动态绑定,属于编译时多态的实现。
重写与隐藏的关系
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏。如果有virtual关键字,函数同名,参数相同,就是“重写”了

指针和引用的区别

从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。
而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。
再来详细解释一下指针和引用的区别(这里要感谢一下某位大神的指点)。
C++里,一个重要的概念是object,不是“面向对象”的对象,是“存储空间”的意思。基本上,凡是有名字的东西都有存储空间。而指针是对存储空间的引用(C++规格书的原话)。比如int p = &a;,那么p里面存着一个指针,它指向a的存储空间。C++的表达式的值分为左值(l-value)和右值(r-value),其中左值是对应存储空间的,而右值就不对应。p这个名字本身对应着存储空间,里面存着一个指针。所以p也是左值。表达式(p)也是左值。意思是“p指向的那个存储空间”。(p)和a对应的存储空间是一样的。因为指针也是值,所以,可以把p的值传来传去。然后再用(p)这样的表达式操作a的存储空间。这是指针的工作原理。总结一下:C++里,用int a;这种方法定义的标识符a对应存储空间,类型就是int。存储空间里存储值。指针本身是一种值,它指向存储空间。(*p)这个表达式对应的存储空间就是p指向的存储空间。

然后再说C++里的“引用”。
如果用int &b = a;这种方式定义,那么b这个标识符的存储空间和a的存储空间是一样的。就是这么简单。b并没有自己的存储空间,可以认为b就是a这个空间的别名。“引用”一旦定义,你就不能让b再去“指向”别的存储空间——在写下int &b = a;的时候,b的存储空间就固定了。但是“指针”则不然。如果定义int *p = &a;,那么p本身是个存储空间,里面存着一个指针。你可以再找另一个指针,存到p里。比如int c; p = &c;这样p就指向c的存储空间了。所以,区别就是“指针本身是一个值,这个值执行一个存储空间;但引用只是另一个存储空间的别名而已,本身并不是单独的值”。以上是指针和引用的语义。

指针传递参数和引用传递参数的区别
答案是:本质上没有区别。

内存对齐

每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数,32位gcc 4.7上默认为8,32位VS2010上默认为8)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
规则:

1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。

如果我们不想编译器自动为我们添加补齐位,可以将对齐系数设为1

#pragma pack(push, 1)
// code...
#pragma pack(pop)

四种智能指针

* auto_ptr
* unique_ptr
* shared_ptr
* weak_ptr

智能指针产生的原因在于解决常规指针可能产生的内存泄漏问题,
将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间
STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文章暂不讨论)。模板auto_ptr是C++98提供的解决方案,C+11已将将其摒弃,并提供了另外两种解决方案。然而,虽然auto_ptr被摒弃,但它已使用了好多年:同时,如果您的编译器不支持其他两种解决力案,auto_ptr将是唯一的选择。
使用auto_ptr仍然会存在,程序将试图删除同一个对象两次的问题。要避免这种问题,方法有多种:

  • 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
  • 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和uniqiie_ptr 的策略,但unique_ptr的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
    当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做 unique_ptr比auto_ptr优秀的地方。
    如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
    如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。

explicit

规避可被单参调用的构造函数引起的隐式类型转换
所有的智能指针类都有一个explicit构造函数,以指针作为参数.因此不能自动将指针转换为智能指针对象,必须显式调用

内存管理

* new和delete
* malloc和free
* 

面向对象

class CMyString
{
public:
    CMyString(char* pData = NULL);
    CMyString(const CMyString &str);
    ~CMyString(void);

private:
    char * m_pData;

对象的大小

继承(为什么要继承?单继承 多继承)

如何实现一个不能被继承的类?

类成员变量初始化顺序

析构函数与构造函数的执行顺序

常见对象->构造函数(缺省构造函数,有参构造函数,复制构造函数)
销毁对象->析构函数

拷贝构造函数

CMyString::CMyString(const CMyString &str)
{
    m_pData = new char[ strlen(str.m_pData) + 1];
    strcpy(m_pData, str.m_pData);
}

为什么拷贝构造函数的参数一定是引用?避免拷贝构造函数不限制的递归复制下去。

赋值构造函数

CMyString& CMyString::operator = (const CMyString &str)
{
    if(this != &str)
    {
        CMyString strTemp(str);
        
        char * pTemp = strTemp.m_pData;
        strTemp.m_pData = m_pData;
        m_pData = pTemp;
    }
    return *this;
}

其实我在想,这里的指针pTemp使用会不会有问题呢?

虚函数与运行时多态

  • 纯虚函数
  • 虚函数列表

函数重载与编译时多态

友元函数

归纳面向对象三大特征:封装 继承 多态

面向对象设计之SOLID五大原则?

泛型编程

编写能够正确处理各种不同数据类型参数的代码,只要参数的数据类型满足特定的语法和语义需求。对于C++而言,实现泛型编程的方式就是使用模板。

template<class T>
class vector {
}

标准模板库(Standard Template Library)

一群优秀的人写的一个优秀的函数库

六大组件

容器

迭代器

适配器

算法

函数对象

空间适配器

vector

map

参考文献

static的作用
C++中指针和引用的区别
C++的重载(overload)与重写(override)
C/C++中const关键字详解
C++智能指针简单剖析
拷贝构造函数的参数为什么必须使用引用类型
C++11新特性——auto的使用

推荐阅读更多精彩内容

  • 前言 把《C++ Primer》读薄系列笔记全集。 目录 第I部分:C++基础 开始学习C++ 变量和基本类型 字...
    尤汐_Jennica阅读 4,258评论 1 39
  • 1. C++基础知识点 1.1 有符号类型和无符号类型 当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值...
    Mr希灵阅读 11,937评论 2 73
  • 1. 让自己习惯C++ 条款01:视C++为一个语言联邦 为了更好的理解C++,我们将C++分解为四个主要次语言:...
    Mr希灵阅读 1,121评论 0 13
  • 球妈又去参加魔鬼故事训练营啦,来我们今天再教一种新的玩法:接力说故事。 四个伙伴,甲、乙、丙、丁。 甲:抛出一个角...
    Vidya阅读 427评论 0 50
  • 中国的土地并不稀缺,好地段才稀缺。 买房子,买的是一个城市某个地段的未来,涉及周边基础设施(商圈,医院,学校,银行...
    牛犇犇unicorn阅读 23评论 0 0