C++八股2
C++八股2
1.C++中const和static的作用
static:
不考虑类的情况
隐藏:当使用
static
修饰全局变量或函数时,它们将仅在定义它们的文件内可见(即具有内部链接性),而没有static
修饰的全局变量和函数则可以在其他文件中通过声明来引用。注:当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
默认初始化为0:无论是未初始化的全局静态变量还是局部静态变量,默认情况下都会被初始化为0,并且这些变量都存储在全局未初始化区。
持久存在与记忆性:如果在函数内部定义了静态变量,那么这个变量在整个程序运行期间一直存在,只会被初始化一次,并且即使函数退出后仍然存在,但它的作用域是局部的。
考虑类的情况
- static成员变量:必须在类外部进行初始化
- static成员函数:没有
this
指针,不能访问类的非静态成员变量或调用非静态成员函数。
const:
- 不考虑类的情况:
- 不可变性:一旦定义了一个
const
常量,就必须同时对其进行初始化,之后其值不能再被修改。 - 参数传递:用const修饰传入参数,则函数保证传入参数不发生改变
- 不可变性:一旦定义了一个
- 考虑类的情况
- const成员变量:必须通过构造函数的初始化列表进行初始化,不能在类定义之外进行初始化。
- const成员函数:这种函数承诺不会修改对象的数据成员(除非数据成员被声明为
mutable
),const
成员函数不可以调用非const
成员函数;const
对象只能调用const
成员函数,而非const
对象既可以调用const
也可以调用非const
成员函数。
1 |
|
2.C++的顶层const和底层const
顶层const(*在左边):表示被修饰的对象本身是一个常量,不能通过这个对象改变它的值。(指针指向不可变)
底层const(*在右边):指针所指向的对象是不可变的。(指针指向的对象是常量)
注:标准的const int
是顶层; const用于声明引用变量是底层
1 | int a = 10; |
注:具有底层const
的指针或引用不能直接赋值给没有const
限定的指针或引用
1 |
|
3.数组名和指针(这里为指向数组首元素的指针)区别
数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。
4.final和override关键字
override:
指定了子类的这个虚函数是重写的父类的,如果函数名输错,编译器会报错
final:
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。
1 | class Base{ |
5.拷贝初始化和直接初始化
直接初始化:直接调用与实参匹配的构造函数来初始化对象。
1
2string str1("I am a string"); // 语句1:直接初始化
string str2(str1); // 语句2:直接初始化,使用另一个对象进行初始化拷贝初始化:首先创建一个临时对象,然后使用拷贝构造函数将这个临时对象的内容拷贝到正在创建的对象中。
1
2string str3 = "I am a string"; // 语句3:拷贝初始化
string str4 = str1; // 语句4:拷贝初始化
6.野指针和悬空指针
野指针:
没有被初始化过的指针
1 | int main(void) { |
解决方案:确保指针在声明时就被初始化。如果暂时没有有效的内存地址可以赋值给指针,应该将其设置为nullptr
。这样,如果尝试解引用一个nullptr
,大多数现代编译器会在运行时抛出异常或给出错误提示,从而帮助开发者快速定位问题。
悬空指针:
最初指向的内存已经被释放了的一种指针
1 | int main(void) { |
解决方案:
- 在释放指针所指向的内存之后,立即将指针设置为
nullptr
。 - C++引入了智能指针(如
std::unique_ptr
和std::shared_ptr
),它们能够自动管理内存的分配和释放,从而有效避免悬空指针的产生。
7.C++中的重载、重写(覆盖)和隐藏的区别
重载: 重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。
重写(override):在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:
- 与基类的虚函数有相同的参数个数
- 与基类的虚函数有相同的参数类型
- 与基类的虚函数有相同的返回值类型
隐藏(hide): 派生类中的函数屏蔽了基类中的同名函数
隐藏和重写的区别: 重写可以体现多态性
1 | class Base { |
输出:
8.C++有哪几种的构造函数
默认构造函数: 不带任何参数的构造函数
1
2
3
4
5
6
7
8
9
10class MyClass {
public:
MyClass() { // 默认构造函数
std::cout << "Default Constructor" << std::endl;
}
};
int main() {
MyClass obj; // 调用默认构造函数
}初始化构造函数: 接受一个或多个参数以初始化对象的构造函数。
1
2
3
4
5
6
7
8
9
10
11class MyClass {
public:
int value;
MyClass(int val) : value(val) { // 初始化构造函数
std::cout << "Parameterized Constructor with value: " << value << std::endl;
}
};
int main() {
MyClass obj(10); // 调用有参数的构造函数
}拷贝构造函数: 使用同一类型的另一个对象来初始化新创建的对象时调用的构造函数。
1
2
3
4
5
6
7
8
9
10
11
12class MyClass {
public:
int value;
MyClass(const MyClass &other) : value(other.value) { // 拷贝构造函数
std::cout << "Copy Constructor, value: " << value << std::endl;
}
};
int main() {
MyClass obj1(20);
MyClass obj2 = obj1; // 调用拷贝构造函数
}移动构造函数(Move和右值引用):用于实现资源转移而非复制,避免不必要的深拷贝操作。它通常与右值引用一起使用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyClass {
public:
int* data;
MyClass(MyClass&& other) noexcept : data(other.data) { // 移动构造函数
other.data = nullptr; //确保原对象不再拥有对资源的所有权,防止悬空指针
std::cout << "Move Constructor" << std::endl;
}
MyClass(int size) : data(new int[size]) {} // 简化的构造函数
~MyClass() { delete[] data; }
};
int main() {
MyClass obj1(100);
MyClass obj2 = std::move(obj1); // 调用移动构造函数
}委托构造函数: 允许在一个构造函数内部调用同一个类的其他构造函数
1
2
3
4
5
6
7
8
9
10
11
12class MyClass {
public:
int value;
MyClass() : MyClass(0) { } // 委托构造函数,委托给下面的构造函数
MyClass(int val) : value(val) { // 初始化构造函数
std::cout << "Parameterized Constructor with value: " << value << std::endl;
}
};
int main() {
MyClass obj; // 调用默认构造函数,但实际通过委托构造函数间接调用了有参数的构造函数
}转换构造函数: 允许编译器隐式地将一种类型的值转换为类的对象
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
class Temperature {
public:
double celsius;
// 转换构造函数:从 double 到 Temperature 的转换
explicit Temperature(double c) : celsius(c) {
std::cout << "Conversion Constructor, Celsius: " << celsius << std::endl;
}
// 显示当前温度
void show() const {
std::cout << "Temperature is " << celsius << " degrees Celsius." << std::endl;
}
};
// 辅助函数,接受一个 Temperature 对象并显示其值
void displayTemperature(const Temperature& temp) {
temp.show();
}
int main() {
// 使用显式构造函数调用创建对象
Temperature t1(36.5); // 正确:显式调用转换构造函数
t1.show();
// 下面这行会导致编译错误,因为构造函数被声明为 explicit
// Temperature t2 = 40.0; // 错误:不允许隐式转换
// 可以通过辅助函数传递 double 值,但需要显式转换
displayTemperature(Temperature(25.0)); // 正确:显式创建临时对象
// 如果去掉 explicit 关键字,则下面的语句也会合法
// Temperature t3 = 37.0; // 如果构造函数不是 explicit,则这是合法的隐式转换
return 0;
}
9.浅拷贝和深拷贝的区别
浅拷贝共享数据: 浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
深拷贝各自拥有独立的数据副本: 开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。
注:浅拷贝在对象的拷贝创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源
10.内联函数和宏定义的区别
- 处理时机:宏定义是在预处理阶段进行简单的文本替换; 内联函数则是在编译时进行处理,并且可以进行参数类型检查。这使得内联函数更加安全和可靠。
- 类型检查与返回值:由于宏只是简单的字符串替换,它无法进行任何类型检查,也无法直接拥有返回值的概念。内联函数支持参数类型检查,确保传入参数的类型正确,并能够像普通函数一样有明确的返回值。
内联函数:
1 |
|
宏:
1 |
|