C++ 基础问题汇总

C++ 在 C 的基础上加了什么

  • 包含全部的 C 语言部分
  • 面向对象编程思想, 封装, 继承, 多态
  • 泛型编程, 模板
  • STL 库

define 宏定义

define 是 C 语言的宏定义, 在编译时会直接进行替换. 注意, 替换不会额外添加其他字符.

1
2
3
4
5
6
7
8
#define SUM(x) 3*x*x+1
int main(){
int i = 3, j = 6;
std::cout << SUM(i+j) << std::endl;
// 输出: 3*i+j*i+j+1 = 3*3+6*3+6+1 = 34
std::cout << SUM((i+j)) << std::endl;
// 输出: 3 * (i+j) * (i+j)+1 = 3 * (3+6) * (3+6)+1 = 3*9*9+1 = 244
}

C++ 的编译和链接过程

C++ 编译和链接

结构体和联合体有什么区别

  1. 结构体和联合体都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合体中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构体的所有成员都存在(不同成员的存放地址不同)
  2. 对于联合体的不同成员赋值, 将会对覆盖其他成员, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的

封装, 继承, 多态, 重载, 重写/覆盖概念解析

  • 封装: 是指将数据和相关的函数放在一起作为一个整体, 一般称之为”类”, 封装的意义在于保护或者放置数据被无意间破坏.
  • 继承: 通常是指类的继承, 主要目的是为了重用代码, 节省开发时间.
  • 多态: 是指”一个接口, 多种形态”, 在 C++ 中的多态主要是通过虚函数实现的.
  • 重载: 是指同名函数具有不同的函数签名(参数个数及参数类型), 程序调用函数时, 会根据不同的函数签名决定调用哪个函数
  • 重写/覆盖: 是指子类重写父类的方法(需要有virtual关键字), 声明完成一致, 实现不同.
  • 隐藏: 派生类实现了一个与基类中同名的函数, 则基类中的同名函数会被隐藏(无论参数列表是否相同)

多态性是指相同对象收到不同消息或不同对象收到相同消息时产生不同的行为. C++ 支持两种多态性, 分别是编译时多态性和运行时多态性:

  • 编译时多态性: 通过重载, 模板等实现.
  • 运行时多态: 通过虚函数的继承实现. 通过声明基类的指针或者引用, 利用该指针或引用指向任意一个子类对象, 调用相应的虚函数, 可以根据指向子类的不同而实现不同的方法.

简单介绍一下 C++ 的继承机制

要注意区分访问权限机制与继承方式机制的区别, 访问机制如下:
public: 可以从类内部, 类外部和派生类中直接访问
protected: 可以从类内部和派生类中进行访问
private: 只能在类的内部进行访问, 注意派生类也不能访问基类的私有成员

继承方式的不同会影响派生类中成员的新的访问权限, 具体影响如下:

  • 公有继承: 基类中的访问权限维持不变. 无法访问父类的私有成员.
  • 保护继承: 会将父类中的公有成员的访问权限变成私有, 其余维持不变. 无法访问父类的私有成员.
  • 私有继承: 会将父类中的公有和保护成员的访问权限变成私有, 无法访问父类的私有成员.
  • 虚拟继承: 虚拟继承时多重继承中特有的概念, 虚拟基类是为了解决多重继承而出现的, 如类 D 继承自类 B 和 类 C, 而类 B 和类 C 都继承自类 A, 此时类 D 中会继承两次 A. 为了节省空间, 可以将 B, C 对 A 的继承定义为虚拟继承, 而 A 成了虚拟基类, 此时只需要继承一次.

friend 可以突破 private 访问权限的限制

C++ 标准规定, 如果派生类中声明的成员与基类的成员同名, 那么, 基类的成员就会被覆盖, 哪怕基类的成员与派生类的成员的数据类型和参数个数完全不同.

基类类的私有成员也会被继承, 但是无法访问, 要想访问, 必须通过父类提供的公有接口

子类不能从父类继承的函数

不能继承的有: (如果不显式定义下面的函数, 编译器会生成默认形式的函数)

  • 构造函数: 虽然不会继承, 但是在派生类调用构造函数前, 会先调用基类的构造函数
  • 析构函数: 先调用派生类的析构函数, 再调用基类的构造函数
  • 拷贝构造函数
  • 赋值操作符=重载函数: 除了赋值运算符重载函数以外, 其他的运算符重载函数都可以被派生类继承.

重载, 隐藏和重写/覆盖的区别

从定义上来说:

  • 重载: 是指允许存在多个 同名 函数, 但是这些函数的 参数签名不同 (参数个数, 参数类型, const)
  • 隐藏: 是指派生类的函数屏蔽了与其同名的基类函数, 注意 只要是同名函数, 不管参数列表是否相同, 基类函数都会被隐藏
  • 重写: 是在继承时体现的, 是指子类重新定义父类虚函数的方法, 其中父类函数必须有virtual关键字, 且不能有static, 子类函数与父类函数签名相同, 且返回值也要相同(或者 返回值协变 ), 访问权限修饰符可以不同.

从实现原理上来说:

  • 重载: 编译器会根据函数不同的函数签名, 对这些同名函数的名称做一些修饰, 然后这些同名函数就成了不同的函数(至少对编译器来说是这样的), 这些修饰后的同名函数的调用, 在编译期间就已经确定了, 因此它们的地址也已经确定了, 因此, 重载与多态无关
  • 隐藏: 由于屏蔽了基类的函数, 因此在调用时会调用派生类的函数
  • 重写: 重写与多态息息相关. 当子类重新定义了父类的虚函数以后, 父类指针会根据赋给它的不同的子类指针, 动态 的调用属于子类的该函数, 这样的函数调用在编译期间是无法确定的(无法给出子类的虚函数的地址). 只有在执行阶段, 子类的函数地址才能够确定.

嵌套类

对嵌套类访问权的控制规则与对常规类相同. 在 Queue 类声明中声明 Node 类并没有赋予 Queue 类任何对 Node 类的访问特征, 也没有赋予 Node 类任何对 Queue 类的访问特权.

访问权限

可被访问的范围:

  • public: 类中函数, 子类函数, 友元函数, 类的对象
  • protected: 类中函数, 子类函数, 友元函数
  • private: 类中函数, 友元函数

继承方式的属性变化:

  • public: 不发生变化
  • protected: public 变为 protected
  • private: public 和 protected 变为 private

范围解析运算符

  • 全局作用域符(::name): 用于名称(类, 类成员, 成员函数, 变量等)前, 表示作用域为全局命名空间
  • 类作用域符(class::name): 用于表示指定类型的作用域范围是具体某个类的
  • 命名空间作用域符(namespace::name): 用于表示指定类型的作用域范围是具体某个命名空间的
1
2
3
4
5
6
7
int count = 10; //全局(::)的count

int main(){
int count = 20; //局部的count
std::cout<<::count; // 输出 10, std为命名空间
std::cout<<count; // 输出 20
}

避免文件被多次编译

  • pragma once: 编程器相关, 有的编译器支持, 有的编译器不支持(大部分都支持).
  • ifndef/define/endif: C/C++ 语言的宏定义, 在所有编译器上都是有效的.

float 与 0 比较时需要注意什么

需要注意精度表示的问题, 不能使用f == 0 而应使用f<0.00001 && f>0.00001类似的语句.

什么情况下会发生运行时错误 Runtime Error

数组越界访问, 除数为0 , 堆栈溢出

数组和指向数组名的指针有什么区别

sizeof 对于指针和数组名的不同反应.

数组的内存空间要么在静态存储区中(全局数组), 要么在栈中. 而指针可以随时指向任意类型的内存块.
在使用sizeof运算符时, 数组返回的是整个数组所占的字节数, 指针返回的是指针变量本身的字节数. C++/C 语言没有办法知道指针所指的内存容量, 除非在申请内存时记住它, 注意当数组作为函数的参数进行传递时, 该数组名就会自动退化为同类型的指针, 也就是说此时再使用sizeof时, 返回的是指针变量的大小, 而不是数组大小

初始化列表(initialization list)

C++11 新特性-初始化列表

  • 哪些情况下只能用初始化列表(initialization list) 而不能用赋值 (assignment)
  • 初始化列表中的初始化顺序是怎样的?

C++是不是类型安全的?

不是, 因为两个不同类型的指针之间可以强制转换.

函数参数的入栈顺序

从右往左, 原因是为了支持可变长参数.
通过堆栈分析可知, 自左向右的入栈方式, 最前面的参数被压在栈底, 除非知道参数个数, 否则是无法通过栈指针的相对位移求得最左边的参数的, 这样就变成了左边参数个数的不确定, 这个动态参数个数的方向相反, 因此, C/C++ 中函数参数采用自右向左的入栈顺序, 主要原因是为了支持可变长的参数形式.