参考:

  1. https://csguide.cn/cpp/
  2. https://zhuanlan.zhihu.com/p/137662774

const

在 C/C++ 中,const 是一个关键字,用于表示常量。const 可以用于修饰变量、函数、指针等,主要作用有以下几种:

修饰变量

const 修饰变量时,该变量将被视为只读变量,即不能被修改。

对于确定不会被修改的变量,应该加上 const,这样可以保证变量的值不会被无意中修改,也可以使编译器在代码优化时更加智能。

这里的变量只读,其实只是编译器层面的保证,实际上可以通过指针在运行时去间接修改这个变量的值。虽然可以这样操作,但这违反了 const的语义,可能会导致程序崩溃或者产生未定义行为(undefined behavior),大家学习了解即可,实际编程中切莫如此操作。因为编译器可能会做一些优化,也就是在用到 const 变量的地方,编译器可能生成的代码直接就替换为常量的值,而不是访问一遍常量的指令。

所以极大可能你虽然修改了值,但是却不起作用!

下面这个例子,展示了使用 const_cast 修改 const 变量的值却不会起作用:

1
2
3
4
5
const int a = 10;
const int* p = &a;
int* q = const_cast<int*>(p);
*q = 20; // 通过指针间接修改 const 变量的值
std::cout << "a = " << a << std::endl; // 输出 a 的值,结果为 10

在上面的例子中,将 p 声明为 const int* 类型,指向只读变量 a 的地址。然后使用 const_cast 将 p 强制转换为 int* 类型的指针 q,从而去掉了 const 限制。接下来,通过指针 q 间接修改了变量 a 的值。但是请注意,即使 a 的值被修改了,但在程序中输出a 的值仍然是 10,正如前面分析,因为 a 是只读变量,所以编译器做了优化,早就把代码实际替换为了下面这样:

std::cout << "a = " << 10 << std::endl;

修饰函数参数,表示函数不会修改参数

const 修饰函数参数时,表示函数内部不会修改该参数的值。这样做可以使代码更加安全,避免在函数内部无意中修改传入的参数值。

尤其是引用作为参数时,如果确定不会修改引用,那么一定要使用 const 引用。

修饰函数返回值

const 修饰函数返回值时,表示函数的返回值为只读,不能被修改。这样做可以使函数返回的值更加安全,避免被误修改。如:

1
2
3
4
5
6
7
8
9
10
const int func() {
int a = 10;
return a;
}

int main() {
const int b = func(); // b 的值为 10,不能被修改
b = 20; // 编译错误,b 是只读变量,不能被修改
return 0;
}

实际上功能与第一条有些类似。

修饰指针或引用

在 C/C++ 中,const 关键字可以用来修饰指针,用于声明指针本身为只读变量或者指向只读变量的指针。

根据 const 关键字的位置和类型,可以将 const 指针分为以下三种情况:

pointer to const

这种情况下,const 关键字修饰的是指针所指向的变量,而不是指针本身。

因此,指针本身可以被修改(意思是指针可以指向新的变量),但是不能通过指针修改所指向的变量。

这种指针有很多种叫法,在 C++ primer 中,你可以叫它 “Pointer to Const”,即 指向常量的指针,同时由于指向的对象为 const 而自己不是 const 类型,因此又可以被归类为一种 底层 const(指向的对象是一个常量)。

1
2
3
4
5
6
const int* p;  // 声明一个指向只读变量的指针,可以指向 int 类型的只读变量
int a = 10;
const int b = 20;
p = &a; // 合法,指针可以指向普通变量
p = &b; // 合法,指针可以指向只读变量
*p = 30; // 非法,无法通过指针修改只读变量的值

还有一点需要注意的是,允许一个指向常量的指针指向一个非常量的对象;所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。这部分详情请参考 《C++ Primer 5th》P56。

const pointer

这种情况下,const 关键字修饰的是指针本身,使得指针本身成为只读变量。

因此,指针本身不能被修改(即指针一旦初始化就不能指向其它变量),但是可以通过指针修改所指向的变量。

1
2
3
4
5
int a = 10;
int b = 20;
int* const p = &a; // 声明一个只读指针,指向 a
*p = 30; // 合法,可以通过指针修改 a 的值
p = &b; // 非法,无法修改只读指针的值

同样地,这种指针也有很多种叫法,且一些中文书籍与《C++ Primer 5th》还有冲突,这里还是以 C++ Primer 为准,叫它 const pointer;其次,由于指针本身为 const 变量,因此可以被称为 顶层const(指针本身是个 const)。

const pointer pointer to const

有点绕口令的感觉了,总之就是结合了以上两点。

这种情况下,const 关键字同时修饰了指针本身和指针所指向的变量,使得指针本身和所指向的变量都成为只读变量。

因此,指针本身不能被修改,也不能通过指针修改所指向的变量。

1
2
3
4
const int a = 10;
const int* const p = &a; // 声明一个只读指针,指向只读变量 a
*p = 20; // 非法,无法通过指针修改只读变量的值
p = nullptr; // 非法,无法修改只读指针的值

const reference

常量引用是指引用一个只读变量的引用,因此不能通过常量引用修改变量的值。

1
2
3
const int a = 10;
const int& b = a; // 声明一个常量引用,引用常量 a
b = 20; // 非法,无法通过常量引用修改常量 a 的值

如何区分?

从左至右顺序理解修饰符会让我们更好地区分以上的几种指针:

  • const int* p; 这种情况下 const 修饰的是 int,因此声明的是指向 const int 的指针。

  • int* const p = &a; 这种情况下 const 修饰的是 p,p 是什么?p是一个 int* 类型的指针,因此声明的是一个 const pointer

修饰成员函数

const 修饰成员函数时,表示该函数不会修改对象的状态(就是不会修改成员变量)。

这样有个好处是,const 的对象就可以调用这些成员方法了,因为 const 对象不允许调用非 const 的成员方法。

也很好理解,既然对象是 const 的,那我怎么保证调用完这个成员方法,你不会修改我的对象成员变量呢?那就只能你自己把方法声明为 const 的了。

普通函数不能修饰为 const

1
2
3
4
5
6
7
8
9
10
class A {
public:
int func() const {
// 编译错误,不能修改成员变量的值
m_value = 10;
return m_value;
}
private:
int m_value;
};

这里还要注意,const 的成员函数不能调用非 const 的成员函数,原因在于 const 的成员函数保证了不修改对象状态,但是如果调用了非 const 成员函数,那么这个保证可能会被破坏。

总之,const 关键字的作用是为了保证变量的安全性和代码可读性。

const 成员函数进阶知识点

  1. const 成员函数可以修改 static 成员变量,因为 static 成员变量是属于类的,而不是属于对象的。

  2. const 成员函数可以修改 mutable 成员变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A {
    public:
    void func() const {
    m_value = 10; // 合法,可以修改 mutable 成员变量
    m_data = 20; // 非法,不能修改普通成员变量
    }

    private:
    mutable int m_value;
    int m_data;
    };

mutable

mutable 是 C++ 中的一个关键字,用于修饰类的成员变量,表示该成员变量可以在 const 成员函数中被修改。它的主要作用是允许在逻辑上保持常量的成员函数中修改某些成员变量,从而提供更大的灵活性。

主要作用:

  1. 允许在 const 成员函数中修改成员变量: 通常,const 成员函数不能修改类的成员变量,但有时我们希望在 const 成员函数中修改某些成员变量,例如用于缓存或统计目的。mutable 关键字可以实现这一点。

  2. 用于 lambda 表达式: 在 lambda 表达式中,mutable 关键字允许修改捕获的变量。默认情况下,lambda 表达式捕获的变量是只读的,使用 mutable 可以使这些变量在 lambda 表达式内部可修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int main() {
    int x = 10;
    auto lambda = [x]() mutable {
    x = 20; // 现在可以修改 x
    std::cout << x << std::endl;
    };
    lambda();
    std::cout << x << std::endl; // 原来的 x 仍然是 10
    return 0;
    }

static

在 C/C++ 中,static 是一个非常重要的关键字,它可以用于变量、函数和类中。

static 修饰全局变量

static 修饰全局变量可以将变量的作用域限定在当前文件中,使得其他文件无法访问该变量。 同时,static 修饰的全局变量在程序启动时被初始化(可以简单理解为在执行 main 函数之前,会执行一个全局的初始化函数,在那里会执行全局变量的初始化),生命周期和程序一样长。

1
2
3
4
5
6
7
8
9
10
11
12
// a.cpp 文件
static int a = 10; // static 修饰全局变量
int main() {
a++; // 合法,可以在当前文件中访问 a
return 0;
}

// b.cpp 文件
extern int a; // 声明 a
void foo() {
a++; // 非法,会报链接错误,其他文件无法访问 a
}

static 修饰局部变量

static 修饰局部变量可以使得变量在函数调用结束后不会被销毁,而是一直存在于内存中,下次调用该函数时可以继续使用。

同时,由于 static 修饰的局部变量的作用域仅限于函数内部,所以其他函数无法访问该变量。

利用这个特性可以实现 Meyer’s Singleton 单例模式。详见 C++ 补完计划(五):C++ 设计模式

1
2
3
4
5
6
7
8
9
10
11
12
void foo() {
static int count = 0; // static 修饰局部变量
count++;
cout << count << endl;
}

int main() {
foo(); // 输出 1
foo(); // 输出 2
foo(); // 输出 3
return 0;
}

static 修饰函数

static 修饰函数可以将函数的作用域限定在当前文件中,使得其他文件无法访问该函数。

同时,由于 static 修饰的函数只能在当前文件中被调用,因此可以避免命名冲突和代码重复定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// a.cpp 文件
static void foo() { // static 修饰函数
cout << "Hello, world!" << endl;
}

int main() {
foo(); // 合法,可以在当前文件中调用 foo 函数
return 0;
}

// b.cpp 文件
extern void foo(); // 声明 foo
void bar() {
foo(); // 非法,会报链接错误,找不到 foo 函数,其他文件无法调用 foo 函数
}

static 修饰类成员变量和函数

static 修饰类成员变量和函数可以使得它们在所有类对象中共享,且不需要创建对象就可以直接访问。

1
2
3
4
5
6
7
8
9
10
11
class MyClass {
public:
static int count; // static 修饰类成员变量
static void foo() { // static 修饰类成员函数
cout << count << endl;
}
};
// 访问:

MyClass::count;
MyClass::foo();

static变量在类的声明中不占用内存,因此必须在.cpp文件中定义类静态变量以分配内存。文件域的静态变量和类的静态成员变量在main执行之前的静态初始化过程中分配内存并初始化;局部静态变量在第一次使用时分配内存并初始化

volatile

volatile 是 C 语言中的一个关键字,用于修饰变量,表示该变量的值可能在任何时候被外部因素更改,例如硬件设备、操作系统或其他线程。

当一个变量被声明为volatile时,编译器会禁止对该变量进行优化,以确保每次访问变量时都会从内存中读取其值,而不是从寄存器或缓存中读取。避免因为编译器优化而导致出现不符合预期的结果。

constvolatile 可以一起使用,volatile 的含义是防止编译器对该代码进行优化,这个值可能变掉的。而 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
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

volatile int counter = 0;

void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
counter++;
}
return NULL;
}

int main() {
pthread_t thread1, thread2;

// 创建两个线程,分别执行increment函数
pthread_create(&thread1, NULL, increment, NULL);
pthread_create(&thread2, NULL, increment, NULL);

// 等待两个线程执行完毕
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);

printf("Counter: %d\n", counter); // 每次输出都不一致

return 0;
}

上面声明了一个volatile int类型的全局变量counter,并创建了两个线程。

每个线程都会对counter变量进行100000次自增操作。

由于counter变量被声明为volatile,编译器不会对其进行优化,确保每次访问都会从内存中读取值。

当然啦,即便是volatile关键字可以确保编译器不对变量进行优化,但上面任然存在并发问题,counter++操作仍然可能导致数据不一致。

为了解决这个问题,需要使用互斥锁、原子操作或其他同步机制。

class & struct

C++ 中为了兼容 C 语言而保留了 C 语言的 struct 关键字,并且加以扩充了含义。

在 C 语言中,struct 只能包含成员变量,不能包含成员函数。

而在 C++ 中,struct 类似于 class,既可以包含成员变量,又可以包含成员函数。

不同点

C++ 中的 structclass 基本是通用的,唯有几个细节不同:

  1. class 中类中的成员默认都是 private 属性的。

  2. struct 中结构体中的成员默认都是 public 属性的。

  3. class 继承默认是 private 继承,而 struct 继承默认是 public 继承。

  4. class 可以用于定义模板参数,struct 不能用于定义模板参数。

使用习惯

实际使用中,struct 我们通常用来定义一些 POD(plain old data)

POD 是 C++ 定义的一类数据结构概念,比如 intfloat 等都是 POD 类型的。

Plain 代表它是一个普通类型,Old 代表它是旧的,与几十年前的 C 语言兼容,那么就意味着可以使用 memcpy() 这种最原始的函数进行操作。

两个系统进行交换数据,如果没有办法对数据进行语义检查和解释,那就只能以非常底层的数据形式进行交互,而拥有 POD 特征的类或者结构体通过二进制拷贝后依然能保持数据结构不变。

也就是说,能用 C 的 memcpy() 等函数进行操作的类、结构体就是 POD 类型的数据。

class 用于定义一些 非 POD 的对象,面向对象编程。

auto & decltype

C++11引入了autodecltype关键字,使用他们可以在编译期就推导出变量或者表达式的类型,方便开发者编码也简化了代码。

auto

auto 可以让编译器在编译器就推导出变量的类型。

1
2
3
4
5
6
auto a = 10;  // 10是int型,可以自动推导出a是int

int i = 10;
auto b = i; // b是int型

auto d = 2.0; // d是double型

用法:

1
2
3
4
int i = 10;
auto a = i, &b = i, *c = &i; // a是int,b是i的引用,c是i的指针,auto就相当于int
auto d = 0, f = 1.0; // error,0和1.0类型不同,对于编译器有二义性,没法推导
auto e; // error,使用auto必须马上初始化,否则无法推导类型
  • auto 的使用必须马上初始化,否则无法推导出类型。

  • auto 在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void func(auto value) {} // error,auto不能用作函数参数

class A {
auto a = 1; // error,在类中auto不能用作非静态成员变量
static auto b = 1; // error,这里与auto无关,正常static int b = 1也不可以
static const auto c = 1; // ok
};

void func2() {
int a[10] = {0};
auto b = a; // ok
auto c[10] = a; // error,auto不能定义数组,可以定义指针
vector<int> d;
vector<auto> f = d; // error,auto无法推导出模板参数
}
  • auto不能用作函数参数。

  • 在类中auto不能用作非静态成员变量。

  • auto不能定义数组,可以定义指针。

  • auto无法推导出模板参数。

1
2
3
4
5
6
7
8
9
10
int i = 0;
auto *a = &i; // a 是 int*
auto &b = i; // b 是 int&
auto c = b; // c 是 int,忽略了引用

const auto d = i; // d 是 const int
auto e = d; // e 是 int

const auto& f = e; // f 是 const int&
auto &g = f; // g 是 const int&
  • 在不声明为引用或指针时,auto 会忽略等号右边的引用类型和cv属性(constvolatile 属性)。

  • 在声明为引用或者指针时,auto会保留等号右边的引用和cv属性。

decltype

上面介绍auto用于推导变量类型,而decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算。

1
2
3
4
5
6
int func() { return 0; }
decltype(func()) i; // i为int类型

int x = 0;
decltype(x) y; // y是int类型
decltype(x + y) z; // z是int类型

decltype 不会像 auto 一样忽略引用和cv属性,decltype 会保留表达式的引用和cv属性。

1
2
3
const int &i = 1;
int a = 2;
decltype(i) b = 2; // b是const int&

对于decltype(exp)有:

  • exp 是表达式,decltype(exp) 和 exp 类型相同。

  • exp 是函数调用,decltype(exp) 和函数返回值类型相同。

  • 其它情况,若 exp 是左值,decltype(exp) 是 exp 类型的左值引用。

1
2
3
4
5
6
int a = 0, b = 0;
decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
decltype(a += b) d = c; // d是int&,因为(a+=b)返回一个左值

d = 20;
cout << "c " << c << endl; // 输出c 20

autodecltype 的配合使用

autodecltype 一般配合使用在推导函数返回值的类型问题上。如:

1
2
3
4
template<typename T, typename U>
return_value add(T t, U u) { // t和v类型不确定,无法推导出return_value类型
return t + u;
}

上面代码由于 t 和 u 类型不确定,那如何推导出返回值类型呢,我们可能会想到这种

1
2
3
4
template<typename T, typename U>
decltype(t + u) add(T t, U u) { // t和u尚未定义
return t + u;
}

这段代码在 C++11 上是编译不过的,因为在 decltype(t+u) 推导时,t和u尚未定义,就会编译出错,所以有了下面的叫做返回类型后置的配合使用方法:

1
2
3
4
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}

返回值后置类型语法就是为了解决函数返回制类型依赖于参数但却难以确定返回值类型的问题。

几种类型转换关键字

在 C 语言中,我们大多数是用 (type_name) expression 这种方式来做强制类型转换,但是在 C++ 中,更推荐使用四个转换操作符来实现显式类型转换:

  • static_cast
  • dynamic_cast
  • const_cast
  • reinterpret_cast

static_cast

用法: static_cast <new_type> (expression)

其实 static_cast 和 C 语言 () 做强制类型转换基本是等价的。

主要用于以下场景:

基本类型之间的转换

将一个基本类型转换为另一个基本类型,例如将整数转换为浮点数或将字符转换为整数。

1
2
int a = 42;
double b = static_cast<double>(a); // 将整数a转换为双精度浮点数b

指针类型之间的转换

将一个指针类型转换为另一个指针类型,尤其是在类层次结构中从基类指针转换为派生类指针。这种转换不执行运行时类型检查,可能不安全,要自己保证指针确实可以互相转换

1
2
3
4
5
class Base {};
class Derived : public Base {};

Base* base_ptr = new Derived();
Derived* derived_ptr = static_cast<Derived*>(base_ptr); // 将基类指针base_ptr转换为派生类指针derived_ptr

引用类型之间的转换

类似于指针类型之间的转换,可以将一个引用类型转换为另一个引用类型。在这种情况下,也应注意安全性。

1
2
3
Derived derived_obj;
Base& base_ref = derived_obj;
Derived& derived_ref = static_cast<Derived&>(base_ref); // 将基类引用base_ref转换为派生类引用derived_ref

static_cast 在编译时执行类型转换,在进行指针或引用类型转换时,需要自己保证合法性。

如果想要运行时类型检查,可以使用 dynamic_cast 进行安全的向下类型转换。

dynamic_cast

用法: dynamic_cast <new_type> (expression)

dynamic_cast在C++中主要应用于父子类层次结构中的安全类型转换。

它在运行时执行类型检查,因此相比于 static_cast,它更加安全。

dynamic_cast 的主要应用场景:

向下类型转换

当需要将基类指针或引用转换为派生类指针或引用时,dynamic_cast可以确保类型兼容性。

如果转换失败,dynamic_cast将返回空指针(对于指针类型)或抛出异常(对于引用类型)。

1
2
3
4
5
class Base { virtual void dummy() {} };
class Derived : public Base { int a; };

Base* base_ptr = new Derived();
Derived* derived_ptr = dynamic_cast<Derived*>(base_ptr); // 将基类指针base_ptr转换为派生类指针derived_ptr,如果类型兼容,则成功

用于多态类型检查

处理多态对象时,dynamic_cast可以用来确定对象的实际类型,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal { public: virtual ~Animal() {} };
class Dog : public Animal { public: void bark() { /* ... */ } };
class Cat : public Animal { public: void meow() { /* ... */ } };

Animal* animal_ptr = /* ... */;

// 尝试将Animal指针转换为Dog指针
Dog* dog_ptr = dynamic_cast<Dog*>(animal_ptr);
if (dog_ptr) {
dog_ptr->bark();
}

// 尝试将Animal指针转换为Cat指针
Cat* cat_ptr = dynamic_cast<Cat*>(animal_ptr);
if (cat_ptr) {
cat_ptr->meow();
}

另外,要使用dynamic_cast有效,基类至少需要一个虚拟函数。

因为,dynamic_cast只有在基类存在虚函数(虚函数表)的情况下才有可能将基类指针转化为子类。

dynamic_cast 底层原理

dynamic_cast的底层原理依赖于运行时类型信息(RTTI, Runtime Type Information)。

C++编译器在编译时为支持多态的类生成RTTI,它包含了类的类型信息和类层次结构。

我们都知道当使用虚函数时,编译器会为每个类生成一个虚函数表(vtable),并在其中存储指向虚函数的指针。

伴随虚函数表的还有 RTTI(运行时类型信息),这些辅助的信息可以用来帮助我们运行时识别对象的类型信息。

《深度探索C++对象模型》中有个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point
{
public:
Point(float xval);
virtual ~Point();

float x() const;
static int PointCount();

protected:
virtual ostream& print(ostream& os) const;

float _x;
static int _point_count;
};

1.png

首先,每个多态对象都有一个指向其vtable的指针,称为vptr

RTTI (就是上面图中的 type_info 结构)通常与vtable关联。

dynamic_cast就是利用RTTI来执行运行时类型检查和安全类型转换。

以下是dynamic_cast的工作原理的简化描述:

  • 首先,dynamic_cast通过查询对象的 vptr 来获取其 RTTI(这也是为什么 dynamic_cast 要求对象有虚函数)
  • 然后,dynamic_cast 比较请求的目标类型与从 RTTI 获得的实际类型。如果目标类型是实际类型或其基类,则转换成功。
  • 如果目标类型是派生类,dynamic_cast 会检查类层次结构,以确定转换是否合法。如果在类层次结构中找到了目标类型,则转换成功;否则,转换失败。
  • 当转换成功时,dynamic_cast 返回转换后的指针或引用。
  • 如果转换失败,对于指针类型,dynamic_cast 返回空指针;对于引用类型,它会抛出一个 std::bad_cast 异常。

因为 dynamic_cast 依赖于运行时类型信息,它的性能可能低于其他类型转换操作(如static_cast),static_cast 是编译器静态转换,编译时期就完成了。

const_cast

用法: const_cast <new_type> (expression)

new_type 必须是一个指针、引用或者指向对象类型成员的指针。

修改 const 对象

当需要修改 const 对象时,可以使用 const_cast 来删除 const 属性。

1
2
3
const int a = 42;
int* mutable_ptr = const_cast<int*>(&a); // 删除const属性,使得可以修改a的值
*mutable_ptr = 43; // 修改a的值

const对象调用非const成员函数

当需要使用const对象调用非const成员函数时,可以使用const_cast删除对象的const属性。

1
2
3
4
5
6
7
8
class MyClass {
public:
void non_const_function() { /* ... */ }
};

const MyClass my_const_obj;
MyClass* mutable_obj_ptr = const_cast<MyClass*>(&my_const_obj); // 删除const属性,使得可以调用非const成员函数
mutable_obj_ptr->non_const_function(); // 调用非const成员函数

不过上述行为都不是很安全,可能导致未定义的行为,因此应谨慎使用。

reinterpret_cast

用法: reinterpret_cast <new_type> (expression)

reinterpret_cast 用于在不同类型之间进行低级别的转换。

首先从英文字面的意思理解,interpret是“解释,诠释”的意思,加上前缀“re”,就是“重新诠释”的意思;

cast 在这里可以翻译成“转型”(在侯捷大大翻译的《深度探索C++对象模型》、《Effective C++(第三版)》中,cast都被翻译成了转型),这样整个词顺下来就是“重新诠释的转型”。

它仅仅是重新解释底层比特(也就是对指针所指针的那片比特位换个类型做解释),而不进行任何类型检查。

因此,reinterpret_cast 可能导致未定义的行为,应谨慎使用。

reinterpret_cast 的一些典型应用场景:

指针类型之间的转换

在某些情况下,需要在不同指针类型之间进行转换,如将一个int指针转换为char指针。

这在 C 语言中用的非常多,C语言中就是直接使用 () 进行强制类型转换

1
2
3
int a = 42;
int* int_ptr = &a;
char* char_ptr = reinterpret_cast<char*>(int_ptr); // 将int指针转换为char指针

inline

inline 是 C++ 中的一个关键字,用于告诉编译器将函数内联展开。内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译器预处理的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数。但内联函数有个缺点是它会增加执行文件大小。所以如果不适当的使用内联函数会造成执行文件特别大。

inline 在使用上有以下一些要注意的点:

  1. 使用inline关键字的函数可能会被编译器忽略而不在调用处展开

    • 如果定义的inline函数过大,为了防止生成的obj文件太大,编译器会忽略这里的inline声明

    • inline是在编译器将函数类容替换到函数调用处,是静态编译的。而虚函数是动态调用的,在编译器并不知道需要调用的是父类还是子类的虚函数,所以不能够inline声明展开,所以编译器会忽略

  2. 头文件中不仅要包含inline函数的声明,还要包含inline函数的定义

    编译器需要把inline函数体替换到函数调用处,所以编译器必须要知道inline函数的函数体是啥,所以要将inline函数的函数定义和函数声明一起写在头文件中,便与编译器查找替换。

  3. 同一个inline函数可以多处声明和定义,但是必须要完全相同

  4. 定义在class声明内的成员函数默认是inline函数

extern

extern 是 C/C++ 中的一个关键字,用于声明一个全局变量或函数,表示该变量或函数是在其他文件中定义的。

  1. extern 可以置于变量声明或者函数声明前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其它文件中寻找其定义。

  2. extern 变量表示声明一个变量,表示该变量是一个外部变量,也就是全局变量,所以 extern 修饰的变量保存在静态存储区(全局区),全局变量如果没有显式初始化,会默认初始化为 0,或者显示初始化为 0 ,则保存在程序的 BSS 段,如果初始化不为 0 则保存在程序的 DATA 段

  3. extern "C" 的作用是为了能够正确的实现 C++ 代码调用 C 语言代码。加上 extern “C” 后,会指示编译器这部分代码按照 C 语言(而不是 C++)的方式进行编译。由于 C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名。 这个功能十分有用处,因为在 C++ 出现以前,很多代码都是 C 语言写的,而且很底层的库也是 C 语言写的,为了更好的支持原来的 C 代码和已经写好的 C 语言库,需要在 C++ 中尽可能的支持 C,而 extern “C” 就是其中的一个策略