参考:https://www.runoob.com/cplusplus/cpp-tutorial.html
本教程旨在提取最精炼、实用的C++面试知识点,供读者快速学习及本人查阅复习所用。
目录
第一章 C++基本语法
C++ 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。
- 对象 - 对象具有状态和行为。例如:一只狗的状态 - 颜色、名称、品种,行为 - 摇动、叫唤、吃。
- 类 - 类可以定义为描述对象行为/状态的模板,对象是类的实例。
- 方法 - 从基本上说,一个方法表示一种行为。一个类可以包含多个方法。可以在方法中写入逻辑、操作数据等动作。
- 即时变量 - 每个对象都有其独特的即时变量。
1.1 C++程序结构
下面给出一段基础的C++程序:
#include <iostream>
using namespace std;
// main() 是程序开始执行的地方
int main()
{
cout << "Hello World" << endl; // 输出 Hello World
return 0;
}
这段程序主要结构如下:
- C++ 语言定义了一些头文件,这些头文件包含了程序中必需的或有用的信息。上面这段程序中,包含了头文件 <iostream>
- using namespace std; 告诉编译器使用 std 命名空间。
- int main() 是主函数,程序从这里开始执行。
1.2 命名空间
- 命名空间这个概念可作为附加信息来区分不同库中相同名称的函数、类、变量等。
- 使用了命名空间即定义了上下文,本质上,命名空间就是定义了一个范围。
1.2.1 定义命名空间
下面通过一个示例来展示如何定义命名空间并使用命名空间中的函数等。
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
int main ()
{
// 调用第一个命名空间中的函数
first_space::func();
// 调用第二个命名空间中的函数
second_space::func();
return 0;
}
1.2.2 using指令
可以使用 using namespace xxxx指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
1.3 预处理器
预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。所有的预处理器指令都是以井号(#)开头,只有空格字符可以出现在预处理指令之前。预处理指令不是 C++ 语句,所以它们不会以分号(;)结尾。
1.3.1 #define预处理
#define 预处理指令用于创建符号常量。该符号常量通常称为宏,指令的一般形式是:
#define macro-name replacement-text
//例如
#define PI 3.14159
可以使用 #define 来定义一个带有参数的参数宏,如下所示:
#include <iostream>
using namespace std;
#define MIN(a,b) (a<b ? a : b)
int main ()
{
int i, j;
i = 100;
j = 30;
cout <<"较小的值为:" << MIN(i, j) << endl;
return 0;
}
1.3.2 条件编译
有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。
条件预处理器的结构与 if 选择结构很像。请看下面这段预处理器的代码:
#ifndef NULL
#define NULL 0
#endif
例如,要实现只在调试时进行编译,可以使用一个宏来实现,如下所示:
#ifdef DEBUG
cerr <<"Variable x = " << x << endl;
#endif
使用 #if 0 语句可以注释掉程序的一部分,如下所示:
#if 0
不进行编译的代码
#endif
下面给出一个示例:
#include <iostream>
using namespace std;
#define DEBUG
#define MIN(a,b) (((a)<(b)) ? a : b)
int main ()
{
int i, j;
i = 100;
j = 30;
#ifdef DEBUG
cerr <<"Trace: Inside main function" << endl;
#endif
#if 0
/* 这是注释部分 */
cout << MKSTR(HELLO C++) << endl;
#endif
cout <<"The minimum is " << MIN(i, j) << endl;
#ifdef DEBUG
cerr <<"Trace: Coming out of main function" << endl;
#endif
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Trace: Inside main function
The minimum is 30
Trace: Coming out of main function
1.4 相关面试题
Q:C++和C的区别
A:设计思想上:
- C++是面向对象的语言,而C是面向过程的结构化编程语言
语法上:
- C++具有封装、继承和多态三种特性
- C++相比C,增加多许多类型安全的功能,比如强制类型转换
- C++支持范式编程,比如模板类、函数模板等
Q:为什么C++支持函数重载而C语言不支持呢
A:在链接阶段,C语言是通过函数本名去寻找函数的实体的,所以当两个函数同名时是无法识别的;而C++会将函数名和函数带的参数转换成编译特征和固有特征,这时候编译器就可以分辨出两个同名不同参的函数。
Q:include头文件双引号””和尖括号<>的区别
A:编译器预处理阶段查找头文件的路径不一样:
- 对于使用双引号包含的头文件,编译器从用户的工作路径开始搜索
- 对于使用尖括号包含的头文件,编译器从标准库路径开始搜索
Q:头文件的作用是什么?
A:
- 通过头文件来调用库功能。
- 头文件能加强类型安全检查。
Q:在头文件中进行类的声明,在对应的实现文件中进行类的定义有什么意义?
A:这样可以提高编译效率,因为分开的话,这个类只需要编译一次生成对应的目标文件,以后在其他地方用到这个类时,编译器查找到了头文件和目标文件,就不会再次编译这个类,从而大大提高了效率。
Q:C++源文件从文本到可执行文件经历的过程
A:对于C++源文件,从文本到可执行文件一般需要四个过程:
- 预编译阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件
- 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
- 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
- 链接阶段:将多个目标文件及所需要的库链接成最终的可执行目标文件
Q:静态链接与动态链接
A:静态链接是在编译期间完成的。
- 静态链接浪费空间 ,这是由于多进程情况下,每个进程都要保存静态链接函数的副本。
- 更新困难 ,当链接的众多目标文件中有一个改变后,整个程序都要重新链接才能使用新的版本。
- 静态链接运行效率高。
动态链接的进行则是在程序执行时链接。
- 动态链接当系统多次使用同一个目标文件时,只需要加载一次即可,节省内存空间。
- 程序升级变得容易,当升级某个共享模块时,只需要简单的将旧目标文件替换掉,程序下次运行时,新版目标文件会被自动装载到内存并链接起来,即完成升级。
Q:C++11有哪些新特性
A:
- auto关键字:编译器可以根据初始值自动推导出类型,但是不能用于函数传参以及数组类型的推导;
- nullptr关键字:nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
- 智能指针:C++11新增了std::shared_ptr、std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
- 初始化列表:使用初始化列表来对类进行初始化
- 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
- atomic原子操作用于多线程资源互斥操作
- 新增STL容器array以及tuple
Q:assert()是什么
A:断言是宏,而非函数。assert 宏的原型定义在 <assert.h>(C)、<cassert>(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前。
#define NDEBUG // 加上这行,则 assert 不可用 #include <assert.h> assert( p != NULL ); // assert 不可用
Q:C++是不是类型安全的?
A:不是,因为两个不同类型的指针之间可以强制转换(用reinterpret cast)。
Q:系统会自动打开和关闭的3个标准的文件是?
A:
- 标准输入----键盘---stdin
- 标准输出----显示器---stdout
- 标准出错输出----显示器---stder
第二章 C++数据操作
2.1 数据类型
2.1.1 基本类型
C++有7种基本的数据类型:
可以使用signed,unsigned,short,long去修饰这些基本类型:
2.1.2 typedef
可以使用 typedef 为一个已有的类型取一个新的名字。例如:
//typedef type newname;
typedef int feet;
feet distance
typedef struct Student {
int age;
} S;
S student;
2.2 变量
2.2.1 变量定义
//type variable_name = value;
extern int d = 3, f = 5; // d 和 f 的声明
int d = 3, f = 5; // 定义并初始化 d 和 f
byte z = 22; // 定义并初始化 z
char x = 'x'; // 变量 x 的值为 'x'
2.2.2 变量声明
可以使用extern关键字在任意地方声明一个变量。
// 变量声明
extern int a, b;
extern float f;
int main ()
{
// 变量定义
int a, b;
float f;
return 0;
}
同样的,函数声明是,提供一个函数名即可,而函数的实际定义则可以在任何地方进行。
// 函数声明
int func();
int main()
{
// 函数调用
int i = func();
}
// 函数定义
int func()
{
return 0;
}
2.2.3 变量作用域
- 在函数或一个代码块内部声明的变量,称为局部变量。
- 在函数参数的定义中声明的变量,称为形式参数。
- 在所有函数外部声明的变量,称为全局变量。
注:当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化为下列值:
2.3 常量
常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。
2.3.1 define预处理器
下面是使用 #define 预处理器定义常量的形式:
#define LENGTH 10
#define WIDTH 5
#define NEWLINE '\n'
2.3.2 const关键字
可以使用 const 前缀声明指定类型的常量,const类型的对象在程序执行期间不能被修改。如下所示:
const int LENGTH = 10;
const int WIDTH = 5;
const char NEWLINE = '\n';
2.4 类型限定符
2.4.1 volatile
volatile 用来修饰变量,表明某个变量的值可能会随时被外部改变,因此使用 volatile 告诉编译器不应对这样的对象进行优化(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值;而被volatile修饰的变量,它不能被缓存到寄存器,每次访问需要到内存中重新读取)。
volatile int n = 10;
2.4.2 restrict
由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。
2.5 存储类
存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期。
2.5.1 auto存储类
auto 关键字用于两种情况:声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。
2.5.2 static存储类
static 存储类指示编译器在程序的生命周期内保持局部变量的存在,而不需要在每次它进入和离开作用域时进行创建和销毁。
- 使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。
- 当 static 修饰全局变量时,会使变量的作用域限制在声明它的文件内。
- 在 C++ 中,当 static 用在类数据成员上时,会导致仅有一个该成员的副本被类的所有对象共享。
2.5.3 extern存储类
extern 存储类用于提供一个全局变量的引用,全局变量对所有的程序文件都是可见的。通常用于当有两个或多个文件共享相同的全局变量或函数的时候。
2.6 运算符
2.7 相关面试题
Q:const的作用
A:
- 修饰变量,说明该变量不可以被修改
- 修饰指针,即常量指针和指针常量
- 常量引用,经常用于形参类型,既避免了拷贝,又避免了函数对值的修改
- 修饰类的成员函数,说明该成员函数内不能修改成员变量
// 类 class A { private: const int a; // 常对象成员,只能在初始化列表赋值 public: // 构造函数 A() : a(0) { }; A(int x) : a(x) { }; // 初始化列表 // const可用于对重载函数的区分 int getValue(); // 普通成员函数 int getValue() const; // 常成员函数,不得修改类中的任何数据成员的值 }; void function() { // 对象 A b; // 普通对象,可以调用全部成员函数、更新常成员变量 const A a; // 常对象,只能调用常成员函数 const A *p = &a; // 常指针 const A &q = a; // 常引用 // 指针 char greeting[] = "Hello"; char* p1 = greeting; // 指针变量,指向字符数组变量 const char* p2 = greeting; // 常量指针即常指针,指针指向的地址可以改变,但是所存的内容不能变 char const* p2 = greeting; // 与const char* p2 等价 char* const p3 = greeting; // 指针常量,指针是一个常量,即指针指向的地址不能改变,但是指针所存的内容可以改变 const char* const p4 = greeting; // 指向常量的常指针,指针和指针所存的内容都不能改变,本质是一个常量 } // 函数 void function1(const int Var); // 传递过来的参数在函数内不可变 void function2(const char* Var); // 参数为常量指针即指针所指的内容为常量不能变,指针指向的地址可以改变 void function3(char* const Var); // 参数为指针常量 void function4(const int& Var); // 参数为常量引用,在函数内部不会被进行修改,同时参数不会被复制一遍,提高了效率 // 函数返回值 const int function5(); // 返回一个常数 const int* function6(); // 返回一个指向常量的指针变量即常量指针,使用:const int *p = function6(); int* const function7(); // 返回一个指向变量的常指针即指针常量,使用:int* const p = function7();
Q:说明define和const在语法和含义上有什么不同?
A:
- #define是C语法中定义符号变量的方法,符号常量只是用来表达一个值,在编译阶段符号就被值替换了,它没有类型;
- const是C++语法中定义常变量的方法,常变量具有变量特性,它具有类型,内存中存在以它命名的存储单元,可以用sizeof测出长度。
Q:static关键字的作用
A:静态变量在程序执行之前就创建,在程序执行的整个周期都存在。可以归类为如下五种:
- 局部静态变量:作用域仅在定义它的函数体或语句块内,该变量的内存只被分配一次,因此其值在下次函数被调用时仍维持上次的值;
- 全局静态变量:作用域仅在定义它的文件内,该变量也被分配在静态存储区内,在整个程序运行期间一直存在;
- 静态函数:在函数返回类型前加static,函数就定义为静态函数。静态函数只是在声明他的文件当中可见,不能被其他文件所用;
- 类的静态成员:在类中,静态成员属于整个类所拥有,对类的所有对象只有一份拷贝,因此可以实现多个对象之间的数据共享,并且使用静态数据成员还不会破坏隐藏的原则,即保证了安全性;
- 类的静态函数:在类中,静态成员函数不接收this指针,因而只能访问类的static成员变量,如果静态成员函数中要引用非静态成员时,可通过对象来引用。(调用静态成员函数使用如下格式:<类名>::<静态成员函数名>(<参数表>);)
Q:请你来说一下C++里是怎么定义常量的?常量存放在内存的哪个位置?
A:常量在C++里使用const关键字定义,常量定义必须初始化。对于局部对象,常量存放在栈区;对于全局对象,编译期一般不分配内存,放在符号表中以提高访问效率;对于字面值常量,存放在常量存储区。
Q:sizeof()和strlen()
A:sizeof是运算符,能获得保证能容纳实现所建立的最大对象的字节大小:
- sizeof 对数组,得到整个数组所占空间大小;
- sizeof 对指针,得到指针本身所占空间大小(4个字节);
- 当一个类A中没有生命任何成员变量与成员函数,这时sizeof(A)的值是1。
strlen()是函数,可以计算字符串的长度,直到遇到结束符NULL才结束,返回的长度大小不包含NULL。
Q:C++ 内存对齐
A:
1)内存对齐的定义
数据项只能存储在地址是数据项大小的整数倍的内存位置上。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
2)使用原因
- 平台原因:不同硬件平台对存储空间的处理上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不允许其在内存中任意存放;
- 性能原因:为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问;如果不按照平台要求对存放数据进行对齐,会发生内存的二次访问,带来存取效率上的损失。
3)内存对齐的规则
- 数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行;
- 结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行;
- 结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。
4)位域
相邻的多个同类型的数(带符号的与不带符号的,只要基本类型相同,也为相同的数),如果他们占用的位数不超过基本类型的大小,那么他们可作为一个整体来看待。不同类型的数要遵循各自的对齐方式。
struct AA { unsigned int a : 10; unsigned char b : 4; short c; }; struct BB { unsigned int a : 10; unsigned int b : 4; short c; };
因此,结构体AA的大小为8字节(a占4字节,b占2字节,c占2字节),BB为8字节(a、b占4字节,c占2字节并补齐)。
Q:强制类型转换运算符
A:static_cast
- 特点:静态转换,编译时执行。
- 应用场合:主要用于C++中内置的基本数据类型之间的转换,同一个继承体系中类型的转换,任意类型与空指针类型void* 之间的转换,但是没有运行时类型检查(RTTI)来保证转换的安全性。
const_cast
- 特点:去常转换,编译时执行。
- 应用场合: const_cast可以用于修改类型的const或volatile属性,去除指向常数对象的指针或引用的常量性。
reinterpret_cast:
- 特点:重解释类型转换,编译时执行。
- 应用场合: 可以用于任意类型的指针之间的转换,对转换的结果不做任何保证。
dynamic_cast:
- 特点:动态类型转换,运行时执行。
- 应用场合:只能用于存在虚函数的父子关系的强制类型转换,只能转指针或引用。对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常
Q:请你说说你了解的RTTI
A:
定义:RTTI(Run Time Type Identification)即通过运行时类型识别,程序能够使用基类的指针或引用来检查着这些指针或引用所指的对象的实际派生类型。
RTTI机制产生原因:C++是一种静态类型语言,其数据类型是在编译期就确定的,不能在运行时更改。然而由于面向对象程序设计中多态性的要求,C++中的指针或引用本身的类型,可能与它实际代表(指向或引用)的类型并不一致。有时我们需要将一个多态指针转换为其实际指向对象的类型,就需要知道运行时的类型信息,这就产生了运行时类型识别的要求。
C++中有两个函数用于运行时类型识别,分别是dynamic_cast和typeid,具体如下:
- typeid函数返回一个对type_info类对象的引用,可以通过该类的成员函数获得指针和引用所指的实际类型;
- dynamic_cast操作符,将基类类型的指针或引用安全地转换为其派生类类型的指针或引用。
Q:explicit(显式)关键字
A:
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化,必须显式初始化
- explicit 修饰转换函数时,可以防止隐式转换,但按语境转换 除外
struct B { explicit B(int) {} explicit operator bool() const { return true; } }; int main() { B b1(1); // OK:直接初始化 B b2 = 1; // 错误:被 explicit 修饰构造函数的对象不可以复制初始化 B b3{ 1 }; // OK:直接列表初始化 B b4 = { 1 }; // 错误:被 explicit 修饰构造函数的对象不可以复制列表初始化 B b5 = (B)1; // OK:允许 static_cast 的显式转换 doB(1); // 错误:被 explicit 修饰构造函数的对象不可以从 int 到 B 的隐式转换 if (b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b6(b1); // OK:被 explicit 修饰转换函数 B::operator bool() 的对象可以从 B 到 bool 的按语境转换 bool b7 = b1; // 错误:被 explicit 修饰转换函数 B::operator bool() 的对象不可以隐式转换 bool b8 = static_cast<bool>(b1); // OK:static_cast 进行直接初始化 return 0; }
Q::: 范围解析运算符
A:该运算符可分为如下三类:
- 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
- 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
- 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的