C++ 基础 - 关键字
const
作用
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针和指针常量;
- 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
- 修饰成员函数,说明该成员函数内不能修改成员变量,除了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);
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
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
- 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定义为static。
- 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。
volatile
volatile 关键字可以用来提醒编译器使用 volatile 声明的变量随时有可能改变,因此编译器在代码编译时就不会对该变量进行某些激进的优化,故而编译生成的程序在每次存储或读取该变量时,都会直接从内存地址中读取数据。
使用 volatile 关键字声明的变量具有三种特性:
- 易变的: 假设有读、写两条语句,依次对同一个 volatile 变量进行操作,那么后一条的读操作不会直接使用前一条的写操作对应的 volatile 变量的寄存器内容,而是重新从内存中读取该 volatile 变量的值。
volatile int nNum = 0; // 将nNum声明为volatile
int nSum = 0;
nNum = FunA(); // nNum被写入的新内容,其值会缓存在寄存器中
nSum = nNum + 1; // 此处会从内存(而非寄存器)中读取nNum的值
2
3
4
- 不可优化的: 不要对 volatile 声明的变量进行各种激进的优化,从而保证程序员写在代码中的指令一定会被执行。
volatile int nNum; // 将nNum声明为volatile
nNum = 1;
printf("nNum is: %d", nNum);
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;
}
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 {// 表示类不能被继承
};
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指示
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
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的指针
}
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;
}
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
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
2
3
4
5
6
7
8
9