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
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
39
40
41
#include <iostream>
using namespace std;

class MyClass {
public:
MyClass(int val) : value(val) {}

// 非const成员函数
void modifyValue(int newVal) {
value = newVal;
}

// const成员函数
int getValue() const {
// 不能修改任何非mutable数据成员
return value;
}

private:
int value;
};

int main() {
const MyClass constObj(10); // 创建一个const对象
MyClass nonConstObj(20); // 创建一个非const对象

// 下面这行会导致编译错误,因为尝试在一个const对象上调用非const成员函数
// constObj.modifyValue(30);

// 可以在一个const对象上调用const成员函数
cout << "constObj value: " << constObj.getValue() << endl;

// 可以在非const对象上调用const成员函数
cout << "nonConstObj value: " << nonConstObj.getValue() << endl;

// 也可以在非const对象上调用非const成员函数
nonConstObj.modifyValue(40);
cout << "After modification, nonConstObj value: " << nonConstObj.getValue() << endl;

return 0;
}

2.C++的顶层const和底层const

顶层const(*在左边):表示被修饰的对象本身是一个常量,不能通过这个对象改变它的值。(指针指向不可变)

底层const(*在右边):指针所指向的对象是不可变的。(指针指向的对象是常量)

注:标准的const int是顶层; const用于声明引用变量是底层

1
2
3
4
5
6
int a = 10;
int* const b1 = &a; // 顶层const,b1本身是一个常量,即b1必须始终指向a
const int* b2 = &a; // 底层const,b2本身可变,但b2指向的对象(即*a)是常量,不可修改
const int b3 = 20; // 顶层const,b3是常量不可变
const int* const b4 = &a; // 前一个const为底层,后一个为顶层,b4不可变且*b4也不可变
const int& b5 = a; // 用于声明引用变量,都是底层const,a的值可通过非const引用改变,但b5无法修改a

注:具有底层const的指针或引用不能直接赋值给没有const限定的指针或引用

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
#include <iostream>

void demonstrateConstRestrictions() {
int a = 10;
const int b = 20;

// 指向普通int类型的指针(无底层const)
int* nonConstPtr;

// 指向const int类型的指针(有底层const)
const int* constPtr = &b;

// 下面这行会导致编译错误:
// error: invalid conversion from 'const int*' to 'int*'
// nonConstPtr = constPtr; // 错误:试图将底层const转换为非const

// 正确的做法是使用const_cast,但要小心使用,确保原始数据确实是可修改的
// 在这个特定情况下,由于'b'是一个const int,这样做实际上是不安全的
// nonConstPtr = const_cast<int*>(constPtr); // 不建议这么做

// 但是,从非const到const的转换总是安全的
const int* safePtr = &a; // 安全:增加const限定不会有问题

std::cout << "Value via safePtr: " << *safePtr << std::endl;
}

int main() {
demonstrateConstRestrictions();
return 0;
}

3.数组名和指针(这里为指向数组首元素的指针)区别

  • 数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。

  • 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。

4.final和override关键字

override:

​ 指定了子类的这个虚函数是重写的父类的,如果函数名输错,编译器会报错

final:

​ 当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错。

1
2
3
4
class Base{    
virtual void foo();};
class A : public Base{
void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写};

5.拷贝初始化和直接初始化

  • 直接初始化:直接调用与实参匹配的构造函数来初始化对象。

    1
    2
    string str1("I am a string"); // 语句1:直接初始化
    string str2(str1); // 语句2:直接初始化,使用另一个对象进行初始化
  • 拷贝初始化:首先创建一个临时对象,然后使用拷贝构造函数将这个临时对象的内容拷贝到正在创建的对象中。

    1
    2
    string str3 = "I am a string"; // 语句3:拷贝初始化
    string str4 = str1; // 语句4:拷贝初始化

6.野指针和悬空指针

野指针:

​ 没有被初始化过的指针

1
2
3
4
5
int main(void) {         
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用,可能会报错
return 0;
}

解决方案:确保指针在声明时就被初始化。如果暂时没有有效的内存地址可以赋值给指针,应该将其设置为nullptr。这样,如果尝试解引用一个nullptr,大多数现代编译器会在运行时抛出异常或给出错误提示,从而帮助开发者快速定位问题。

悬空指针:

​ 最初指向的内存已经被释放了的一种指针

1
2
3
4
5
int main(void) {   
int * p = nullptr;
int* p2 = new int;
p = p2; delete p2;
} // 此时p和p2都变成了悬空指针

解决方案

  • 在释放指针所指向的内存之后,立即将指针设置为nullptr
  • C++引入了智能指针(如std::unique_ptrstd::shared_ptr),它们能够自动管理内存的分配和释放,从而有效避免悬空指针的产生。

7.C++中的重载、重写(覆盖)和隐藏的区别

重载: 重载是指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。

重写(override):在派生类中覆盖基类中的同名函数,重写就是重写函数体要求基类函数必须是虚函数且:

  • 与基类的虚函数有相同的参数个数
  • 与基类的虚函数有相同的参数类型
  • 与基类的虚函数有相同的返回值类型

隐藏(hide): 派生类中的函数屏蔽了基类中的同名函数

隐藏和重写的区别: 重写可以体现多态性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Base {
public:
virtual void foo(int x) { cout << "Base: " << x << endl; }
void foo(int x, int y) { cout << "Base: " << x << ' ' << y << endl; }
};

class Derived : public Base {
public:
void foo(int x) { cout << x << endl; } // 重写了基类中的foo(int x)
void foo(int x, int y) { cout << x << ' ' << y << endl; } // 隐藏了基类中的foo(int x, int y)
};

int main() {
Base *pb = new Derived;
pb->foo(1); // 调用的是Derived::foo(int x),因为Base::foo(int x)是虚函数且被重写
pb->foo(1, 2); // 调用的是Base::foo(int x, int y),因为Derived::foo(int x, int y)隐藏了基类的同名函数

delete pb;
return 0;
}

输出:

image-20250316202637727

8.C++有哪几种的构造函数

  • 默认构造函数: 不带任何参数的构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class MyClass {
    public:
    MyClass() { // 默认构造函数
    std::cout << "Default Constructor" << std::endl;
    }
    };

    int main() {
    MyClass obj; // 调用默认构造函数
    }
  • 初始化构造函数: 接受一个或多个参数以初始化对象的构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class 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
    12
    class 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
    15
    class 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
    12
    class 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
    #include <iostream>

    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
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
// 定义一个内联函数来计算两个整数的和
inline int add(int a, int b) {
return a + b;
}
int main() {
int num1 = 5;
int num2 = 10;
// 使用内联函数计算两数之和
int sum = add(num1, num2);
std::cout << "The sum of " << num1 << " and " << num2 << " is " << sum << "." << std::endl;
return 0;
}

宏:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
// 定义一个宏来计算两个整数的和
#define ADD(a, b) ((a) + (b))
int main() {
int num1 = 5;
int num2 = 10;
// 使用宏计算两数之和
int sum = ADD(num1, num2);
std::cout << "The sum of " << num1 << " and " << num2 << " is " << sum << "." << std::endl;
return 0;
}