C++关键字知识点总结:const、volatile、static、extern、assert()、struct、typedef struct等
const
const 在C++中是用来修饰内置类型变量,自定义对象,成员函数,返回值,函数参数。
作用
- 修饰变量,说明该变量不可以被改变;
- 修饰指针,分为指向常量的指针和指针常量,还有指向常量的指针常量;
- 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对传入值得修改(建议使用);
- 修饰成员函数,说明该成员函数内不能修改成员变量。
使用
// An highlighted block
var foo = 'bar';
class A
{
private:
const int a; // 常对象成员,只能在初始化列表中赋值
public:
// 构造函数
A() : a(0) { }; // 默认构造函数
A(int x) : a(x) { }; // 初始化列表
// const可用于对重载函数的区分
int getValue(); // 普通成员函数,普通对象调用此函数
int getValue() const;
// 常成员函数,常量对象调用此函数,不得修改类中的任何数据成员的值(除开被mutable修饰的成员)
// Effective C++中建议“运用const成员函数实现出其non-const孪生兄弟”,用到强制类型转换
};
void function()
{
// 对象
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 function1(const int Var); // 传递过来的参数在函数内不可变,拷贝传递
void function2(const char* Var); // 参数指针所指内容为常量
void function3(char* const Var); // 参数指针为指针常量
void function4(const int& Var); // 引用参数在函数内为常量,不可修改
// 函数返回值
const int function5(); // 返回一个常数
const int function5_1(){return 0;} //此处加不加const都一样,返回值是内置类型
const int* function6(); // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7(); // 返回一个指向变量的指针常量,使用:int* const p = function7();
volatile
Volatile关键字跟const对应相反,是易变的,容易改变的意思。
易变性:
所谓的易变性,在汇编层面反映出来,就是两条语句,下一条语句不会直接使用上一条语句对应的volatile
变量的寄存器内容,而是重新从内存中读取。
“不可优化”特性:
volatile
告诉编译器,不要对我这个变量进行各种激进的优化,甚至将变量直接消除,保证程序员写在代码中的指令,一定会被执行。
”顺序性”:
能够保证volatile
变量间的顺序性,编译器不会进行乱序优化。volatile
变量与非volatile变量的顺序,编译器不保证顺序,可能会进行乱序优化。同时,C/C++ volatile
关键词,并不能用于构建happens-before语义,因此在进行多线程程序设计时,要小心使用volatile
,不要掉入volatile
变量的使用陷阱之中。
static
作用:隐藏、持久、初始化、
- 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在main函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统会用默认初始值初始化;(用
static
定义全局变量时,放在源文件而不是头文件,防止其他源文件引用时对此项又申请一个内存) - 修饰普通函数,表明函数的作用范围:仅在定义该函数的文件内才能使用。在多人开发的项目时,为了防止与他人定义函数重名,可将函数定位为
static
; - 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员,静态数据成员是类的成员,而不是对象的成员;
- 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,类的静态成员函数是属于整个类而非类的对象,所以它没有
this
指针,这就导致 了它仅能访问类的静态数据和静态成员函数。
备注:
- 存储在静态存储区的2种变量:全局变量、static变量,相比于全局变量,
static
可以控制变量的可见范围:比如static
定义在某个文件内,则作用域为整个文件。static局部变量在函数内定义,则作用域仅在该函数内,但由于存储在静态区,所以生存期为整个源程序。 - 静态数据成员是静态存储的,所以必须对它进行初始化。 (程序员手动初始化,否则编译时一般不会报错,但是在Link时会报错误);
- 静态成员初始化与一般数据成员初始化不同:<数据类型><类名>::<静态数据成员名>=<值>;
- 为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。
extern
用法:
- 变量的声明和定义中,C++中某些变量的声明和定义不在一个文件内,被分离开;
extern int i; //声明i而非定义,不申请存储空间
int j; //声明并定义i,申请存储空间
extern int v = 2;
int v = 2; //这两个语句效果完全一样,都是v的定义,都申请存储空间
- extern函数声明
如果函数的声明中带有关键字extern
,仅仅是暗示这个函数可能在别的源文件里定义,没有其它作用。
注意:若把全局变量的声明和定义放在一起,连接时会报错误:
“因为你把全局变量
g_str
的定义放在了头文件之后,test1.cpp这个模块包含了test1.h所以定义了一次g_str,而test2.cpp也包含了test1.h所以再一次定义了g_str
,这个时候连接器在连接test1和test2时发现两个g_str
。”
只在头文件中做声明,真理总是这么简单
3. 多个文件中共享const
对象,又不希望编译器为每个文件单独生成一个变量
方法:对于const
变量不管是声明还是定义都添加extern
关键字,这样只需要定义一次就可以了
//file1.cpp定义并初始化和一个常量,该常量能被其他文件访问
extern const int bufferSize = function();
//file1.h头文件
extern const int bufferSize; //与file1.cpp中定义的是同一个
- 模板的控制实例化
避免多个文件中实例化相同的模板的额外开销,可以通过显式实例化来避免这种开销。
extern template declaration; // 实例化声明,不定义,不申请存储,定义在另外一个地方
template declaration; // 实例化定义,申请存储
extern template class vec<string>; //声明
template int sum(const int, const int); //定义
//当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码,将一个实例化声明为extern就表示承诺在程序的其他位置有该实例化的一个
//非extern定义。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
extern与static
(1)extern
表明该变量在别的地方已经定义过了,在这里要使用那个变量;
(2)static
表示静态的变量,分配内存的时候, 存储在静态区,不存储在栈上面;
(3)static
作用范围是内部连接的关系, 和extern
有点相反.它和对象本身是分开存储的,extern
也是分开存储的,但是extern
可以被其他的对象用extern
引用,而static
不可以,只允许对象本身用它. 具体差别首先,static
与extern
是一对“水火不容”的家伙,也就是说extern
和static
不能同时修饰一个变量;其次,static
修饰的全局变量声明与定义同时进行,也就是说当你在头文件中使用static
声明了全局变量后,它也同时被定义了;最后,static
修饰全局变量的作用域只能是本身的编译单元,也就是说它的“全局”只对本编译单元有效,其他编译单元则看不到它;
extern与const
C++中const修饰的全局常量具有跟static相同的特性,即它们只能作用于本编译模块中,但是const
可以与extern
连用来声明该常量可以作用于其他编译模块中, 如
extern const char g_str[]; // test.h
const char g_str[] = "123456"; // test.cpp 然后在原文件中别忘了定义
extern “C”
- 被
extern
限定的函数或变量是 extern 类型的; - 被
extern "C"
修饰的变量和函数是按照C语言方式编译和连接的; extern "C"
的作用是让 C++ 编译器将 extern “C” 声明的代码当做C语言代码处理,可以避免 C++ 因符号修饰导致代码不能喝 C语言库中的符号进行链接的问题。
#ifdef __cplusplus
extern "C" {
#endif
void *memset(void *, int, size_t);
#ifdef __cplusplus
}
#endif
assert()
断言,是宏,而非函数。assert 宏的原型定义在 <assert.h>
(C),<cassert.h>
(C++)中,其作用是如果它的条件返回错误,则终止程序执行。可以通过NDEBUG来关闭 assert,但是需要在源代码的开头,include <assert.h>
之前。assert()
多用于在函数开始处检验传入参数的合法性,且一个assert()
只检验一个条件;
#include <assert.h> // 原型定义
void assert( int expression ); // 先计算 expression,如果其值为假,则向 stderr 打印出错信息,然后调用 abort 终止程序
// test.cpp
#define NDEBUG // 加上这行,则 assert 不可用
#include <assert.h>
assert (p != NULL); // assert 不可用
//------------------------------------------------------
assert(i++ < 100); // 错误,不能使用改变环境的语句。比如执行之前i=100,那么此时i++这条命令就没有执行
assert(i < 100); // 应该改成这样
i++;
sizeof()
- sizeof 对数组,得到整个数组所占空间大小;(所占内存字节数)
- sizeof 对指针,得到指针本身所占空间大小;(与所指对象没有任何关系)
- 当字符数组表示字符串时,其
sizeof
值将'/0'
计算进去; sizeof
也可对函数调用求值,返回的是函数返回值类型的大小,函数不会被调用;(函数得带实参,不能对void 函数求值…)
char *b = "helloworld";
char *c[10];
double *d;
int **e;
void (*pf)();
cout<<"char *b = /"helloworld/""<<sizeof(b)<<endl;//指针指向字符串,值为4
cout<<"char *b "<<sizeof(*b)<<endl; //指针指向字符,值为1
cout<<"double *d "<<sizeof(d)<<endl;//指针,值为4
cout<<"double *d "<<sizeof(*d)<<endl;//指针指向浮点数,值为8
cout<<"int **e "<<sizeof(e)<<endl;//指针指向指针,值为4
cout<<"char *c[10] "<<sizeof(c)<<endl;//指针数组,值为40
cout<<"void (*pf)(); "<<sizeof(pf)<<endl;//函数指针,值为4
#pragma pack(n)
设定结构体、联合体以及类成员变量以n字节方式对齐 #pragma pack(n)
,可选值为1,2,4,8,16
#pragma pack(push) // 保持对齐状态
#pragma pack(4) // 设定为 4 字节对齐,或者 #pragma pack(push, 4)
struct test()
{
char m1;
double m4;
int m3;
};
#pragma pack(pop) // 恢复对齐状态
位域
类可以将其(非静态)数据成员定义为位域(bit-field),在一个位域中含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备传递二进制数据时,通常会用到位域。位域的作用主要是节省内存资源,使数据结构更紧凑。
- 位域在内存中的布局是与机器有关的;
- 位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定;
- 取地址运算符
&
不能作用于位域,任何指针都无法指向类的位域;
Bit mode: 2; // mode 占 2 位
struct 和 typedef struct
C 中
定义1个结构体类型要使用typedef struct...
// c
typedef struct Student{
int age;
}S; // 使用 Student stu1; 或者 S stu1;
// c 等价于-------------------------------------------
struct Student{
int age;
};
typedef struct Student S; // 此时S等价于struct Student,但两个标识符名称空间不相同。
// 另外还可以定义与struct Student 不冲突的 void Strdent(){}
C++中
由于编译器定位符号的规则(搜索规则)改变,导致不同于C语言。
如果在类标识符空间定义了struct Student{...};
,使用Student me;
时,编译器将搜索全局标识符表,Student
未找到,则在类标识符内搜索。
即表现为可以使用Student
, 也可以使用struct Student
,如下:
//cpp
struct Student{
int age;
}stu1; // stu 此时是一个变量,使用时可以直接访问 stu1.age
void f(Student me); // 正确,struct 关键词可省略
若定义了与Student
同名函数之后,则Student
只代表函数,不代表结构体,如下:
typedef struct Student{
int age;
}S; // S 此时是一个结构体类型,注意与上面对比
void Student(){} // 正确,定以后 “Student” 只代表此函数
// void S(){} // 错误,符号 “S” 已经被定义为一个 “struct Student” 的别名
int main()
{
Student(); // 此时,调用Student() 函数
struct Student me; // 使用同名的结构体,需要在前面加上 struct 或者: S me;
return 0;
}
C++ 中的 struct
结构体在函数中的作用不是简便,最主要的作用是封装。封装的好处是可以再次利用;
结构体默认的对齐方式:
- 各成员变量在存放的时候根据结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节VC会自动填充。同时VC为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,所以在为最后一个成员变量申请空间后,还会根据需要自动补充空缺的字节。
- VC对变量存储的一个特殊处理。为了提高CPU的存取速度,VC对一些变量的起始地址做了“对齐”处理。默认下,VC规定各成员变量存放的起始地址相对于结构的起始地址的偏移量必须为该变量的类型所占用的字节数的倍数。
注:与#pragmapack(n)
结合时,情况会有变化:
- 如果变量的字节数小于等于n,那么采用默认对齐方式;
- 如果变量字节数大于n,偏移量就是n的倍数;
- 结构总的大小也有限制,如果所有的变量字节数都比 n 小,那么结构总的空间占用为字节边界数的倍数,反之必须为 n 的倍数;
C++ 中的 struct 和 class
总的来说,struct
更适合看成是一个数据结构的实现体, class更适合看成是一个对象的实现体;
区别
最本质的一个区别是默认的访问控制
默认的继承访问权限:struct 是 public,class 是 private;
struct
作为数据结构的实现体,它默认的数据访问控制是 public
的,而 class
作为对象的实现体,它默认的成员变量访问控制是 private
的;
union联合
联合是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合的特点:
默认访问控制符为 public;
可以含有构造函数、析构函数;
不能含有引用类型的成员;
不能继承自其他类,不能作为基类;
不能含有虚函数;
匿名 union 在定义所在作用域可直接访问 union 成员;
匿名 union 不能包含 protected 成员或 private 成员;
全局匿名联合必须是静态 static 的;
#include<iostream>
using namespacestd;
union UnionTest{
UnionTest(): i(10){};
int i;
double d;
};
static union{
int i;
double d;
};
int main()
{
UnionTest u;
union{
int i;
double d;
}
cout<< u.i << endl; // 输出 UnionTest 联合的i为10;
::i = 20;
cout<< ::i << endl; // 输出全局静态匿名联合的i为20;
}
union的内存分配
#include <iostream>
using namespace std;
union Test{
struct{
int x; int y; int z;
}s;
int k;
}myUnion;
int main()
{
myUnion.s.x = 4;
myUnion.s.y = 5;
myUnion.s.z = 6;
myUnion.k = 0;
cout<< myUnion.s.x <<endl;
cout<< myUnion.s.y <<endl;
cout<< myUnion.s.z <<endl;
cout<< myUnion.k <<endl;
}
//运行结果: 0 5 6 0............k赋值时将s.x的位置覆盖
explicit(显式)关键字
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化;
- explicit 修饰转换函数时,可以防止隐式转换,但 按语境转换 除外;
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 a(1); // 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; // Wrong: 被explicit修饰构造函数的对象不可以复制初始化
B b3{1}; // OK:列表初始化
B b4 = {1}; // Wrong: 被explicit修饰构造函数的对象不可以复制列表初始化
B b5 = (B)1; // OK: 允许static_cast 的显式转换
doB(1); // Wrong: 被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;
}
【参考文献】
https://www.cnblogs.com/god-of-death/p/7852394.html
https://www.cnblogs.com/broglie/p/5524932.html
https://www.cnblogs.com/yuxingli/p/7821102.html
https://www.cnblogs.com/huolong-blog/p/7587711.html
https://www.cnblogs.com/zhengfa-af/p/8144786.html