关键字
# 关键字
Owner: -QVQ-
# override
确保重写了父类的虚函数
struct Derived : public Base {
void doSomething(int i) override { // ERROR,编译器报警没有正确覆写Base::doSomething
std::cout << "This is from Derived with " << i << std::endl;
}
};
2
3
4
5
编译器帮你检查是否这个函数是对父类的重写,如果不是则报错
# const关键字
一个语义约束,编译器会强制实施这个约束,允许程序员告诉编译器某值是保持不变的
const是对变量自己约束,其他函数复制这个变量不受影响
**注意:**const 关键字不能与 static 关键字同时使用。static 关键字修饰静态成员函数,静态成员函数不含有 this 指针,即不能实例化,const 成员函数必须具体到某一实例。
const是和编译器的约定,const int a; auto b = a;//此时b不是const类型,在程序运算时是不知道const的
const比起#define来说,语义更明确,可以用于更复杂的类型,且有作用域的限制
const成员创建时必须初始化,如果不初始化直接输出会报错
const不能修饰类的定义,不能修饰类外函数
# const修饰类中的常量
所有实例类只需存放一个就好,直接用const并不能起到这样的效果
用静态对象,或者枚举
# const修饰普通变量
const int a = 1;
int* b = static_cast<int*>(&a);//将直接错误
int* b = (int*)&a;//不会报错,但实际上丢失了const
*b = 2;
cout<<a;//此时a的值是1还是2是不确定的,系统可能认为a是const型而直接从缓存拿值而不是去内存拿
//这取决于优化级别和编译器类型
2
3
4
5
6
这是因为编译器认为a一直是1,从而产生意想不到的行为
如果不想让编译器察觉到上面对const的操作,可以加上volatile关键字,使得编译器不改变对a的操作
volatile const int a = 7;
int *p = (int*)&a;
*p = 8;
cout<<a;//此时输出为8
2
3
4
const修饰指针
指针常量(右定向):int* const p = &a;
//可修改指向的内容,指针指向不可改
常量指针(左定值):const in *p = 8;
//指针指向可修改,指向的值不可修改
const int * const p = &a;
//均不可修改
# const参数传递
- 值传递的const修饰传递
void cpu(const int a)
//作为参数传递
- const参数为指针时
void cpu(int *const a)
//指针在函数内始终指向一个地址
- 自定义类型的参数传递(自定义类型需要构造函数)
void Cmf(const Test& _tt)
比起void Cmf(Test _tt)
传入临时对象,还要调用次拷贝函数,用const+引用可以提高性能的同时避免被修改数据
const作为函数的入参时,外部函数的调用时传入的参数并不受影响,const只是限定函数范围内对他的使用
void abc(const int i){//只限制函数内部的使用
}
int i = 0;
abc(i); //const不限制外部的传入
//同理
int a = 1;
const int b = a; //const对当前变量的限制,只适用于当前变量,无关传入的值
2
3
4
5
6
7
8
# const作返回值
const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
const int Cmf()
如果希望返回的值不被修改则应该用引用返回的方式const 修饰引用作为返回值,可以让返回值不能作为左值使用,既不能被赋值,也不能被修改。
对于
int & min( int & i, int & j);
在调用时可以作为左值使用,min(a,b)=4
,这显然不合适因此变为
const int & min( int & i, int & j);
那么就不能对min()进行赋值操作了const 修饰指针作为返回值,可以让指向的内容不能被修改
const int* getptr(void) { int *p = (int *)0xCC; return p; } int main(){ const int *p = getptr();//ok int *q = getptr();//error }
1
2
3
4
5
6
7
8
9
原则上,const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。
引用作为函数返回值,可以防止复制拷贝,但通常是将返回值作为入参传入指针的,引用做返回值没有什么必要
# const修饰类成员函数
const不能修饰类的定义
防止成员函数修改被调用对象的值
const修饰类的成员函数时只对类中定义的变量有const限制,指针为指针常量类型,成员函数可以修改外部传入的参数
class tes{
public:
void test(){}
}
class stu {
public:
stu() {
b = (int*)malloc(sizeof(int));
}
void test(int *a1, int a2)const {
*a1 = 1;//ok
a1 = NULL;//ok
a2 = 1;//ok
*b = 2;
//b = NULL;//错误,不能修改指针指向
//a.test();报错,不能调用成员类的非const函数
c->test();//ok,指针可以调用
}
tes a;
tes* c;
int* b;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果有个const成员函数想修改对象中的某个成员,可以使用mutable关键字
class Test
{
public:
Test(int _m,int _t):_cm(_m),_ct(_t){}
void Kf()const
{
++_cm; // 错误
++_ct; // 正确
}
private:
int _cm;
mutable int _ct;
};
2
3
4
5
6
7
8
9
10
11
12
13
注意声明的常量类中,要调用的函数也必须是常量才行,所以编译通过的程序const修饰的类调用的函数不会改变值
class tes{
public:
void test(){}
}
class stu{
public:
void test(){}//这里加上const就不会报错
void test1(){
a.test();//报错
b->test();
}
void test2()const{
//a.test();报错
b->test();
}
tes a;
tes* b;
}
const stu a;
//a.test();//此时将报错
a.test1();//a为常量不影响a中的函数调用类中类的函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
auto关键字并不会继承const特性,但是auto&会,所以工程中当必须用auto&提高性能时,也许可以用强转去掉const特性
# const修饰全局变量
放在.h文件中不会引发重定义的问题,因为是内部链接,每个cpp复制一份有自己的空间
# static
用来控制变量的存储方式和可见性。
只能初始化变量、函数、类中成员,不能作为类、结构体的关键字
# 意义
可以节省内存,对多个对象来说,静态数据成员只存储一处,供所有对象共用
对于普通函数:在函数内部定义的变量,当程序执行到它的定义处时,编译器为它在栈上分配空间,函数在栈上分配的空间在此函数执行结束时会释放掉
对于静态函数:将函数中此变量的值保存至下一次调用时,记录函数状态
(使用全局变量能达到同样的效果,但此变量的访问范围过大,考虑到数据安全性)
在 C++ 中,需要一个数据对象为整个类而非某个对象服务,同时又力求不破坏类的封装性,即要求此成员隐藏在类的内部,对外不可见时,可将其定义为静态数据。记录类状态,实现单例模式
对于静态全局函数,可以使函数只在当前文件内生效,避免多个文件的函数名冲突
# 静态数据的存储
全局(静态)存储区:**DATA 段(全局初始化区)**存放初始化的全局变量和静态变量;**BSS 段(全局未初始化区)**存放未初始化的全局变量和静态变量。程序运行结束时自动释放
其中BBS段在程序执行之前会被系统自动清0,所以未初始化的全局变量和静态变量在程序执行之前已经为0。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化
# static 的内部实现机制:
静态数据成员按定义出现的先后顺序依次初始化,注意静态成员嵌套时,要保证所嵌套的成员已经初始化了。消除时的顺序是初始化的反顺序。
初始化时机:
全局静态变量在main函数之前初始化,为了线程安全
局部静态变量,在函数执行到此语句才执行初始化
# 修饰全局变量
全局变量默认有外部链接性,在其他文件可通过extern的声明使用
全局静态变量的作用域是声明此变量所在的文件,其他文件用extern也无法访问
static
关键字只能用于修饰类的成员,如静态变量、静态函数或静态常量。
static的初始化只能在.cpp中,不能在头文件中,那样可能会出现多次定义,const和enum类型例外
特点:
- 静态变量和静态局部变量都在全局数据区分配内存
- 这个全局变量、函数只能在本文件中访问,即便是 extern 外部声明也不可以。
- 未经初始化的静态全局变量和函数体外被声明的自动变量会被程序自动初始化为0
# 修饰静态局部变量:
- 全局变量分配内存,直到程序运行结束,但作用域为局部作用域
- 静态局部变量一般在声明处初始化,即以后的函数调用不再进行初始化;如果没有显式初始化,会被程序自动初始化为 0
# 修饰类中成员
- 静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。
- 可通过**<类名>::<静态成员名>**来直接访问静态函数,而不需要先对类实例化
- 类的静态成员函数不可调用类中的非静态成员函数
- 类的静态成员变量在使用前必须先初始化,否则会编译通过,链接错误
如果修饰静态内部类,静态内中没有指向外围类的引用(普通类中类有,用他访问外部类的变量)
类中static实现双向链表
const int MAX_NAME_SIZE = 30; class Student { public: Student(const char* pszName); ~Student(); public: static void PrintfAllStudents(); private: char m_name[MAX_NAME_SIZE]; Student* next; Student* prev; static Student* m_head;//始终维持着上一个节点的位置 }; Student::Student(const char* pszName) { strcpy_s(this->m_name, pszName); //头插法建立双向链表 this->next = m_head; this->prev = NULL; if (m_head != NULL) m_head->prev = this; m_head = this; } Student::~Student()//析构过程就是节点的脱离过程 { if (this == m_head) //该节点就是头节点。 { m_head = this->next; } else { this->prev->next = this->next; this->next->prev = this->prev; } } void Student::PrintfAllStudents() { for (Student* p = m_head; p != NULL; p = p->next) printf("%s\n", p->m_name); } Student* Student::m_head = NULL; int main() { Student studentA("AAA"); Student studentB("BBB"); Student studentC("CCC"); Student studentD("DDD"); Student student("MoreWindows"); Student::PrintfAllStudents(); 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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 建议
若全局变量仅在单个C文件中访问,则可以将这个变量修改为静态全局变量,以降低模块间的耦合度;
若全局变量仅由单个函数访问,则可以将这个变量改为该函数的静态局部变量,以降低模块间的耦合度;
# 初始化
static修饰全局变量/函数内的变量不需要初始化,会自动为0,
static修饰类内对象必须初始化,不然链接失败
# extern
# 原理
编译时会为每个声明的变量和函数创建符号表,extern将让编译器在符号表中记录其为外部链接
在编译后的链接中,会解析符号表,解决外部符号引用
# 使用
如果全局变量不在文件的开头定义,有效的作用范围将只限于其定义处到文件结束。
关键字 extern 对该变量作“外部变量声明”,表示该变量是一个已经定义的外部变量。
起声明作用,扩展变量的作用域
对于全局变量,如果不指定,默认就是外部变量类型,局部变量为自动变量类型
当全局变量在外部声明时,编译器会无法判断是声明还是定义,因此需要加上extern表示这是声明(函数不需要,函数有函数体来判断是不是声明还是定义)
对于a文件的一个全局变量,被b,c文件引用,会被重定义,因此最好头文件只声明,不定义, 在.c里定义
例子
#include <stdio.h>
int max(int x,int y);
int main(void)
{
int result;
/*外部变量声明*/
extern int g_X;
extern int g_Y;
result = max(g_X,g_Y);
printf("the max value is %d\n",result);
return 0;
}
/*定义两个全局变量*/
int g_X = 10;
int g_Y = 20;
int max(int x, int y)
{
return (x>y ? x : y);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在多项目的情况下可以避免重复定义
/****max.c****/
#include <stdio.h>
/*外部变量声明*/
extern int g_X ;
extern int g_Y ;
int max()
{
return (g_X > g_Y ? g_X : g_Y);
}
/***main.c****/
#include <stdio.h>
/*定义两个全局变量*/
int g_X=10;
int g_Y=20;
int max();
int main(void)
{
int result;
result = max();
printf("the max value is %d\n",result);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# extern “c”
# 底层机制
名称修饰:禁止cpp中对函数和变量名称的修饰,使cpp编译的函数和c有相同的名称和调用约定
# 嵌套使用带来的问题
指定编译和连接规约,但不影响指定函数内部实现的语义(内部依然是c++标准)
- 将#include语句放到extern “c”外面,确保不会出现重复嵌套
如果嵌套的文件都有用extern ”c”,那么就会出现extern ”c”的嵌套
extern "c"{
extern "c"{
extern "c++"{
}
}
}
2
3
4
5
6
且编译器默认以最里面的嵌套为标准,
另一个问题是可能改变函数声明的链接规范
//a.h c++文件
void foo(int);
void a(int);
//b.h c++文件
extern "C" {
#include "a.h"
void b();
}
//main.cpp
#include "b.h"
//本意上是b.h希望用c的方式展开,a.h希望用c++的方式展开,但实际上a.h也变为c的方式了
2
3
4
5
6
7
8
9
10
11
12
13
14
- 为了防止c语言包含c++的头文件编译时,无法识别extern “c”(这是c++的语法),因此通常采用如下格式将c++代码包含起来
#ifdef __cplusplus
extern "c" {
#endif
/*…………*/
#ifdef __cplusplus
}
#endif
2
3
4
5
6
7
8
9
# explicit
# 禁止隐式转换:
在构造函数声明的时候加上explicit关键字,能够禁止隐式转换,此时上面两个例子的问题都能解决
class str {
public:
explicit str(const char* ch) {
//本意上是放入这个字符串
cout <<"malloc" << ch << endl;
}
explicit str(int n) {
//本意上是初始化n个字符的空间
cout << "malloc" <<n<<"char"<<endl;
}
};
int main() {
str s1(10);//ok
str s2("hello");//ok
str s3 = 10;//编译不通过,隐式转换被禁止了
str s4 = 'a';//编译不通过,隐式转换被禁止了
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
对于没有使用explicit
的对象就是implicit类型,即可以隐士转换
# restrict
对于如下代码
int add1(int* a, int* b)
{
*a = 10;
*b = 12;
return *a + *b;
}//当a的地址等于b的地址时,返回结果与预想的不同(取决于编译器)
2
3
4
5
6
优化
int add2(int* __restrict a, int* __restrict b)
{//无论a和b的地址相不相等
*a = 10;
*b = 12;
return *a + *b ;
}//restrict关键字使得无需访问内存,直接将12写入寄存器,提高了性能
2
3
4
5
6
# assert关键字
void assert(int expression);
#include<assert.h>
计算表达式expression的值为假(0),则先向stderr打印一条出错信息,然后通过调用abort来终止程序
频繁的调用会极大的影响程序的性能,通常用于debug版本
在release版本,通过在包含#include 的语句之前插入 #define NDEBUG
来禁用assert调用
使用场景
- 可以在预计正常情况下程序不会到达的地方放置断言。(如assert(0);)
- 使用断言测试方法的前置条件和后置条件;
- 前置条件:代码执行前必须具备的特性;
- 后置条件:代码执行后必须具备的特性;
每个类只检验一个条件,能更直观的判断哪个条件失败 不使用改变环境的语句,例如i++ assert和后面的语句应该空一行
# sizeof
- 对于有虚函数的类,父类虚函数在子类的虚函数的前面
类中没有数据则大小为1,如果有,则大小为数据的大小,如果有虚函数,再+8字节
- 对于数组大小的计算:
void test(int a[10]) {//作为函数的参数传递时,数组变了
cout << sizeof(a) << endl;//4,数组a变为了指针,输出指针大小
}
int main()
{
int a[10];
cout << sizeof(a) << endl;//40,整个数组的大小
test(a);
}
2
3
4
5
6
7
8
9
10
int a[2];
sizeof(a);//8
sizeof(a[0]);//4
2
3
- sizeof是运算符,大多时候由编译器直接计算出结果,不同编译器不同
const int c=10;
int* b = (int*) & c;//试图修改值
cin >>* b;
int a[c];
cout << sizeof(a) << endl;//40,并没有访问这个值
2
3
4
5
- 不对能动态申请的空间计算长度
- 对于多文件,在定义时必须表明其大小,否则编译报错
//a.h
int a[];
//a.c
int a[10];
//main.c
#include "a.h"
sizeof(a);//error
2
3
4
5
6
7
- sizeof(表达式/函数),只计算表达式/函数结果的类型大小,并不执行表达式/函数
int a = 0, b = 9;
cout << sizeof(b /a) << endl;//4
2
# try-catch
- 语言层面的报错:由C++运行时环境或标准库检测到,程序的逻辑或运行时行为触发,可以通过异常处理机制来捕获和处理。
- Linux系统级别的报错:由操作系统内核或系统调用检测到,通常与系统资源或硬件有关,可能影响整个系统的稳定性,可以通过检查系统调用的返回值和信号处理机制来捕获和处理。
throw 表达式;//该语句抛出一个异常,可以有多个throw,但至少要有一个
try {
语句组 //首先执行try的代码,如果没有异常不执行catch里面的语句
}
catch(异常类型) {
异常处理代码
}
...
catch(异常类型) {
异常处理代码
}//catch可以有多个,但至少要有一个
catch(…){//能狗捕获任何类型的异常
}
2
3
4
5
6
7
8
9
10
11
12
异常的声明列表
void func() throw (int, double, A, B, C);
上面的写法表明 func 可能拋出 int 型、double 型以及 A、B、C 三种类型的异常。异常声明列表可以在函数声明时写,也可以在函数定义时写。两处都写,则两处应一致。
void func() throw ();
表示不会抛出任何异常
异常类型:
bad_typeid:使用一个空指针
bad_cast:在用 dynamic_cast 进行从多态基类对象(或引用)到派生类的引用的强制类型转换时,如果转换是不安全的,则会拋出此异常
bad_alloc:用new分配内存没有空间了时
ios_base::failure:
out_of_range:用 vector 或 string 的 at 成员函数根据下标访问元素时,下标越界
try {
Bad bad;
throw 2;
cout<<"dfs";//上面抛出了异常,这一行将不会执行
} catch(...) {
std::cout << "Never print this\n";
}
2
3
4
5
6
7
当throw抛出异常,或者程序代码本身有逻辑错误抛出异常,那么会立即退出当前的函数栈,后面的代码将不会执行,同时,依次向调用栈上层递归查找最近的catch处理函数,如果没有找到catch,将终止程序
# noexcept
标注一个函数是否会抛出异常
noexcept
//等同于noexcept(true)
noexcept(true)
、//不会抛出异常
noexcept(false)
、//会抛出异常
noexcept(expression)
、//检查一个函数是否有能力抛出异常
// noexcept 标识符
void foo() noexcept(true) {
throw 4;
}
// noexcept 标识符
void bar() noexcept(false) {
throw 4;
}
int main(void) {
// noexcept 函数
cout << boolalpha << noexcept(foo()) << endl; // true
cout << boolalpha << noexcept(bar()) << endl; // false
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
throw()
//可能会抛出异常,c++20中放弃了这种写法