C++ 基础 - 关键字

const

作用

  1. 修饰变量,说明该变量不可以被改变;
  2. 修饰指针,分为指向常量的指针和指针常量;
  3. 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  4. 修饰成员函数,说明该成员函数内不能修改成员变量,除了mutable修饰的变量。

使用

DETAILS
// 类
class A
{
public:
    A() : a(0) { };             // 构造函数
    A(int x) : a(x) { };        // 初始化列表

    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
    const int& fun() const;     // 第一个const代表该函数的返回值无法被改变。第二个const代表该函数不会对调用者内部成员进行更改。
private:
    const int a;                // 常对象成员,只能在初始化列表赋值
};

void fun()
{
    // 对象
    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 p3 = greeting;          // 常指针,指向字符数组变量
    const char* const p4 = greeting;    // 常指针,指向字符数组常量
}

// 函数
void fun1(const int Var);           // 传递过来的参数在函数内不可变
void fun2(const char* Var);         // 参数指针所指内容为常量
void fun3(char* const Var);         // 参数指针为常指针
void fun4(const int& Var);          // 引用参数在函数内为常量

// 函数返回值
const int fun5();      // 返回一个常数
const int* fun6();     // 返回一个指向常量的指针变量,使用:const int *p = fun6();
int* const fun7();     // 返回一个指向变量的常指针,使用:int* const p = fun7();

//const修饰函数时的重载

//实际上没有区别,因为函数调用的时候,存在形实结合的过程,所以不管有没有const都不会改变实参的值。所以不能通过编译,提示重定义。
void fun(const int i);
void fun(int i);

//char *a 中a指向的是一个字符串变量,而const char *a指向的是一个字符串常量,所以当参数为字符串常量时,调用第二个函数,而当函数是字符串变量时,调用第一个函数。
void fun(char *a);    
void fun(const char *a);

//这两个都是指向字符串变量,不同的是char *a是指针变量 而char *const a是指针常量。所以不能通过编译,提示重定义。
void fun(char *a);  
void fun(char * const a);  

//原因是第一个i引用的是一个变量,而第二个i引用的是一个常量,两者是不一样的,类似于上面的指向变量的指针的指向常量的指针。
void fun(int &i);  
void fun(const int &i);   

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

mutable

用于在类的成员函数中修饰非静态成员变量,允许在常量成员函数中修改这些成员变量的值(不违反const限定符)。

static

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定义为static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

volatile

volatile 关键字可以用来提醒编译器使用 volatile 声明的变量随时有可能改变,因此编译器在代码编译时就不会对该变量进行某些激进的优化,故而编译生成的程序在每次存储或读取该变量时,都会直接从内存地址中读取数据

使用 volatile 关键字声明的变量具有三种特性:

  • 易变的: 假设有读、写两条语句,依次对同一个 volatile 变量进行操作,那么后一条的读操作不会直接使用前一条的写操作对应的 volatile 变量的寄存器内容,而是重新从内存中读取该 volatile 变量的值
volatile int nNum = 0;  // 将nNum声明为volatile
int nSum = 0;
nNum = FunA();      // nNum被写入的新内容,其值会缓存在寄存器中
nSum = nNum + 1;    // 此处会从内存(而非寄存器)中读取nNum的值
1
2
3
4
  • 不可优化的: 不要对 volatile 声明的变量进行各种激进的优化,从而保证程序员写在代码中的指令一定会被执行。
volatile int nNum;  // 将nNum声明为volatile
nNum = 1;
printf("nNum is: %d", nNum);
1
2
3

在上述代码中,如果变量 nNum 没有声明为 volatile 类型,则编译器在编译过程中就会对其进行优化,直接使用常量“1”进行替换(这样优化之后,生成的汇编代码很简介,执行时效率很高)。而当使用 volatile 进行声明后,编译器则不会对其进行优化,nNum 变量仍旧存在,编译器会将该变量从内存中取出,放入寄存器之中,然后再调用 printf() 函数进行打印。

  • 顺序执行的: 能够保证 volatile 变量间的顺序性不会被编译器进行乱序优化。由于 volatile 关键字的“顺序执行特性”并非会完全保证语句的顺序执行(如 volatile 变量与非 volatile 变量之间的操作;又如一些 CPU 也会对语句的执行顺序进行优化)。

其他:

  • const 可以是 volatile (如只读的状态寄存器)
  • 指针可以是 volatile

struct 和 class

总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。

区别

  • 默认的访问控制(最本质)

    • 默认的继承访问权限。struct 是 public 的,class 是 private 的。
    • struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
  • 成员函数

    • 类可以包含成员函数,这些函数可以操作类的私有成员,并且可以实现类的行为和功能。
    • 结构体也可以有成员函数,但是它们的主要目的是为了实现一些操作,而不是定义类似于类的行为。
  • 继承

    • 类可以通过继承实现子类与父类之间的关系,可以使用公共、保护或私有继承来控制成员的访问权限。
    • 结构体也可以继承,但由于其成员默认是公共的,继承可能导致访问权限问题。
  • 构造函数和析构函数

    • 类可以拥有构造函数和析构函数,用于对象的初始化和清理。
    • 结构体也可以有构造函数和析构函数,但是它们的使用场景通常是比较简单的数据封装。

注意:C++ class和 C struct 结合时,最好将C struct作为C++ class的成员,而不是通过继承。

explicit(显式)关键字

  • explicit 修饰构造函数时,可以防止隐式转换和复制初始化
  • explicit 修饰转换函数时,可以防止隐式转换
DETAILS
struct A
{
    A(int) {}
    operator bool() const { return true; }
};

struct B
{
    explicit B(int) {}
    explicit operator bool() const { return true; }
};

void doA(A a) {}

void doB(B b) {}

int main()
{
    A a1(1);     // OK:直接初始化
    A a2 = 1;    // OK:复制初始化
    A a3{1};     // OK:直接列表初始化
    A a4 = {1};  // OK:复制列表初始化
    A a5 = (A)1; // OK:允许 static_cast 的显式转换
    doA(1);      // OK:允许从 int 到 A 的隐式转换
    if (a1);                            // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a6(a1);                   // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a7 = a1;                    // OK:使用转换函数 A::operator bool() 的从 A 到 bool 的隐式转换
    bool a8 = static_cast<bool>(a1); // OK :static_cast 进行直接初始化

    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;
}
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

friend 友元类和友元函数

  • 能访问私有成员
  • 破坏封装性
  • 友元关系不可传递
  • 友元关系的单向性
  • 友元声明的形式及数量不受限制

override、final

  • override 用于表示当前函数重写了基类的虚函数。
  • final 用于禁止类继承、禁止重载虚函数。
class Base {
public:
    virtual void g(); // 虚函数
    virtual void h() = 0; // 纯虚函数
};

class Derived : public Base {
    void g() override; // 表示派生类重写基类虚函数
    void h() final; // 表示不可再被派生类进一步重载
};

class Student final {// 表示类不能被继承
};
1
2
3
4
5
6
7
8
9
10
11
12
13

virtual

用于声明虚基类、虚函数。虚函数=0时,则为纯虚函数,纯虚函数所在的类称为抽象类。

const、constexpr

  • const 表示所修饰的对象或变量不能被改变。
  • constexpr 用于生成常量表达式,常量表达式主要是允许一些计算发生在编译时,而不是运行的时候。

noexcept

C++11中,用于声明一个函数不可以抛出任何异常。

using

  • using声明一次之引入命名空间中的一个成员。
  • using指示使得某个特定的命名空间中所有的名字都可见。
using std::cout;//using声明
using std;//using指示
1
2

头文件与using声明或指示

头文件如果在其顶层作用域中含有using指示或using声明,则会将名字注入到所有包含了该头文件的文件中。通常情况下,头文件应该只负责定义接口部分的名字,而不定义实现部分的名字。因此,头文件最多只能在它的函数或命名空间内使用using指示或using声明。

对命名空间内部名字的查找遵循常规的查找规则:由内向外依次查找每个外层作用域。外层作用域也可能是一个或多个嵌套的命名空间,直到最外层的全局命名空间查找过程终止。

重载与using声明

一个using声明包括了重载函数的所有版本以确保不违反命名空间的接口。

using NS::print

尽量少使用 using 指示 污染命名空间

一般说来,使用 using 声明比使用 using 指示更安全,这是由于它只导入了指定的名称。如果该名称与局部名称发生冲突,编译器将发出指示。using指示导入所有的名称,包括可能并不需要的名称。如果与局部名称发生冲突,则局部名称将覆盖名称空间版本,而编译器并不会发出警告。另外,名称空间的开放性意味着名称空间的名称可能分散在多个地方,这使得难以准确知道添加了哪些名称。

enum

C++ 包含两种枚举:限定作用域和不限定作用域

enum class open_modes {input,output,append};//限定作用域
int i = open_modes::input;//限定作用域的枚举类型不会进行隐式转换

enum color {red,yellow,green};//不限定作用域
int j = color::red;//不限定作用域的枚举类型的枚举成员隐式转换成int
1
2
3
4
5

typeid

typeid 运算符用来获取一个表达式的类型信息。

应用: 运行时类型识别(run-time type identification, RTTI)

  • typeid 运算符,用于返回表达式的类型
  • dynamic_cast 运算符,用基类指针或引用安全地转换成派生类的指针或引用

类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。

DETAILS
if(Derived *dp = dynamic_cast<Derived*>(bp)){
    // 使用dp指向的 Derived 对象
}
else{
    // 使用bp指向的 Base 对象
}

if(typeid(*bp) == typeid(*dp)){
    //bp 和 dp指向同一类型对象
}

if(typeid(*bp) == typeid(Derived)){
    //bp 实际指向Derived对象
}

if(typeid(bp) == typeid(Derived)){
    //永远不被指向,bp是指向Base的指针
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

应用: type_info类

DETAILS
#include <iostream>
#include <typeinfo>

class Base {
public:
    virtual void print() {}
};

class Derived : public Base {
public:
    virtual void print() {}
};

int main() {
    Base* basePtr = new Derived();
    
    // 使用typeid运算符获取类型信息
    const std::type_info& type = typeid(*basePtr);
    
    // 输出类型的名称
    std::cout << "Type name: " << type.name() << std::endl;
    
    // 比较类型
    if (type == typeid(Base)) {
        std::cout << "Type is Base" << std::endl;
    } else if (type == typeid(Derived)) {
        std::cout << "Type is Derived" << std::endl;
    } else {
        std::cout << "Type is unknown" << std::endl;
    }
    
    delete basePtr;
    
    return 0;
}
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

结果

Type name: class Derived
Type is Derived
1
2

extern "C"

  • 被 extern 限定的函数或变量是 extern 类型的
  • extern "C" 修饰的变量和函数是按照 C 语言方式编译和链接的

extern "C" 的作用是让 C++ 编译器将 extern "C" 声明的代码当作 C 语言代码处理,可以避免 C++ 因符号修饰导致代码不能和C语言库中的符号进行链接的问题。

extern "C" 使用

#ifdef __cplusplus
extern "C" {
#endif

void *memset(void *, int, size_t);

#ifdef __cplusplus
}
#endif
1
2
3
4
5
6
7
8
9