本文整理了我复习时容易混淆或不易记忆的关键字用法和注意事项。欢迎指正和讨论,谢谢。
目录
(2)用 const 修饰指针变量:注意区分const修饰的是谁
(3)用 const 修饰函数参数:传递过来的参数在函数内不可以改变
(4) 用 const修饰成员函数:在参数列表后面加const
(1)区别以下几种变量?const int a; int const a; const int *a; int *const a;
(1)const_cast 用法:const →非const
一、volatile
volatile(不稳定的)限定一个对象可被外部进程(操作系统、硬件或并发线程等)改变,声明时的语法如下:
int volatile nVint;
定义volatile变量,说明该变量可能会意想不到的被修改,这样编译器就不会去假设这个变量的值,而是每次都重新读取该变量的值,而不是使用寄存器中的备份值。
1)并行设备的硬件寄存器
2)中断服务子程序中用到的非自动变量
3)多线程应用中被多个任务共享的变量
二、static【限定访问域,延长生命周期】
被修饰的变量存储位置在静态区。生命周期长。
- C:
- 隐藏。修饰全局变量和函数,该全局变量和函数只能在该源文件中可见,可以解决命名冲突的问题。
- 变量初始化为零。
- 保持局部变量内容的持久性。修饰局部变量,局部变量生命周期和源文件一样长
- C++:
- static数据成员是与该类相关联而不与该类对象相关联,一个对象修改了该数据成员的值,其他对象也可以看见。
- static修饰函数,没有this指针,
【具体】
(1) static修饰局部变量(静态局部变量)
对于静态局部变量,相对于一般局部变量其生命周期长,直到程序运行结束而非函数调用结束,且只在第一次被调用时定义;程序开始时分配空间,结束时释放空间,而不需要在每次它进入和离开作用域时进行创建和销毁;
(2) static修饰全局变量(静态全局变量)
对于静态全局变量,相对于全局变量其可见范围被缩小,只能在本文件(声明它的文件)中可见;
(3) static修饰函数
修饰函数时作用和修饰全局变量相同,都是为了限定访问域。只有本文件内的代码才可访问它,它的名字(变量名或函数名)在其它文件中不可见。注意:.h文件中声明static全局函数不能加static,在.cpp文件中声明全局函数需要加static。
(4) static修饰类成员:静态成员变量
静态成员变量和静态成员函数不属于任何一个对象,是所有类实例所共有。
- 也就是说当某个类的实例修改了该静态成员变量,其修改值为该类的其它所有实例所见(多对象数据共享);
- 静态数据成员必须在类的外部定义,类的静态成员变量必须在声明它的文件范围内进行初始化才能使用,private 类型的也不例外。通过
<classname>::<static member>
来使用。
(5) static修饰类成员:静态成员函数
- 静态成员函数可以不加类名直接引用静态数据成员,但引用类中说明的非静态成员需要加上对象名。
- 因为当调用一个对象的非静态成员函数时:系统把对象的起始地址赋给成员函数的this指针。
- 静态成员函数不属于某一对象,没有this指针,就无法对一个对象的非静态成员默认访问(引用数据成员时不指定对象名)。所以要想引用就要:
对象名.非静态成员
;
- static的数据记忆性可以满足函数在不同调用期的通信,也可以满足同一个类的多个实例间的通信。未初始化时,static变量默认值为0。
- 每个对象占用的存储空间 = 对象的非静态数据成员的总和,而成员函数和静态数据成员不占存储空间。
三、const(常量的)所修饰的对象或变量不能被改变
1. const 用法
(1)用 const 声明一个变量:带类型的常量
意味着该变量就是一个带类型的常量,可以代替 #define,且比 #define 多一个类型信息,且它执行内链接,可放在头文件中声明;但在 C 中,其声明则必须放在源文件(即 .C 文件)中,在 C 中 const 声明一个变量,除了不能改变其值外,它仍是一个变量。如:
const double pi(3.14159);
const double pi = 3.14159;
(2)用 const 修饰指针变量:注意区分const修饰的是谁
- 只有一个const,如果const位于*左侧,表示指针所指数据是常量,不能通过解引用修改该数据;指针本身是变量,可以指向其他的内存单元。
int a1=3; const int * a4 = &a1; ///const data,non-const pointer
- 只有一个const,如果const位于*右侧,表示指针本身是常量,不能再指向其他内存地址;指针所指的数据可以通过解引用修改。
int * const a5 = &a1; ///non-const data,const pointer
- 两个const,*左右各一个,表示指针和指针所指数据都不能修改
int const * const a6 = &a1; ///const data,const pointer
const int * const a7 = &a1; ///const data,const pointer
(3)用 const 修饰函数参数:传递过来的参数在函数内不可以改变
void test ModifyConst(const int x)
{ x = 5; } ///编译出错
- const 只能修饰输入参数。
- 如果输入参数采用“指针传递”,那么加const 修饰可以防止意外地改动该指针,起到保护作用。
void StringCopy(char *strDestination, const char *strSource);
- 其中 strSource 是输入参数(不能改内容),strDestination 是输出参数(可更改)。
- 给 strSource 加上const修饰后,如果函数体内的语句试图改动 strSource 的内容,编译器将指出错误。
- 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数(对于非内部数据类型,效率低),影响不到实参的值,该输入参数本来就无需保护,所以不要加const 修饰。
void Func(const A &a); // const传引用例子。 非内部数据类型的输入参数
void Func(int x); // 非const传值示例。 对于内部数据类型的输入参数,不需要加const,“值传递”和“引用传递”的效率几乎相当。
- “引用传递”:仅借用一下参数的别名而已,不需要产生临时对象,但有可能改变参数a,想防止意外改动,加const修饰即可。
- 但是要注意:函数究竟是要返回一个对象的拷贝,还是仅仅返回一个别名就行了。这种应用,一般只出现在类的赋值函数中,目的是为了实现链式表达。
A & operate = (const A & other); // 赋值函数 A a, b, c; // a, b, c 为A 的对象 a = b = c; // 正常的链式赋值 (a = b) = c; // 不正常的链式赋值,但合法
👆如果将赋值函数的返回值加const 修饰,那么该返回值的内容不允许被改动。上例中,语句 a = b = c 仍然正确,但是语句 (a = b) = c 则是非法的。
- 但是要注意:函数究竟是要返回一个对象的拷贝,还是仅仅返回一个别名就行了。这种应用,一般只出现在类的赋值函数中,目的是为了实现链式表达。
(4) 用 const修饰成员函数:在参数列表后面加const
- const成员函数 不能修改任何的成员变量(mutable修饰的变量除外)
- const成员函数 不能调用非const成员函数,因为非const成员函数可能会修改成员变量
- const成员函数 不可以修改对象的数据,不管对象是否具有const性质.
class List {
private:
int length;
mutable bool is_valid;
public:
int GetLength () const; // 常量成员函数
bool DeleteNode(const int index); // 普通成员函数, index在函数内不可修改
bool CheckList() const; // 常量成员函数
}
// const成员函数int List::GetLength() const{
DeleteNode(2); // 错误:不能调用非const成员函数
return length++; // 错误:不能修改成员变量(mutable类型除外)
}
bool CheckList() const{
if(length >= 1) then return is_valid =true;
else return is_valid = false; // 正确!因为is_valid为mutable类型,可被常量成员函数更改
}
- const数据成员的初始化必须在构造函数初始化列表中初始化,而不可以在构造函数函数体内初始化(不能在类声明中初始化const数据成员)
(5) 用 const修饰对象
- const对象只能调用const成员函数。
- const对象的成员是不可修改的,然而const对象通过指针维护的对象却是可以修改的.
const List myList; // 成员变量不能被修改。
myList.GetLength(); // 正确,GetLength是const常函数,它返回链表长度,的确没有改变属性值的行为,检验通过
myList.DeleteNode(3); // 错误,DeleteLength是非const成员函数,
// 👆很有可能改变对象中length(链表长度)这个值,这不符合const对象的规定。
(6) 用 const修饰函数返回值
- 指针传递
- 如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
const char * GetString(void); // 如下语句将出现编译错误: char *str = GetString(); // 正确的用法是: const char *str = GetString();
-
即:如果返回const data,non-const pointer,返回值也必须赋给const data,non-const pointer。因为指针指向的数据是常量不能修改。
const int * mallocA(){ ///const data,non-const pointer int *a = new int(2); return a; } int main() { const int *p = mallocA(); ///int *b = mallocA(); ///编译错误 return 0; }
- 如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
- 值传递 不需要const
2. 相关辨析
(1)区别以下几种变量?const int a; int const a; const int *a; int *const a;
int const a
和const int a
均表示定义 整型常量a 。const int *a
,其中a为指向int型常量的指针,const在 * 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)int *const a
,依旧是指针类型,表示a为指向整型变量的常指针。(看成const(a),对指针const)
(2)什么是常引用?
- 常引用可以理解为常量指针,形式为
const typename & refname = varname
。 - 常引用下,原变量值不会被别名所修改。
- 原变量的值可以通过原名修改。
- 常引用通常用作只读变量别名或是形参传递。
3. const 与 define 与enum
(1)怎样才能建立在整个类中都恒定的常量呢?
使用类中的enum枚举常量。
- enum 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。
- 枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如PI=3.14159)。
enum em{ SIZE1 = 100, SIZE2 = 200}; // 枚举常量,sizeof(em)是
(2)使用const代替define的好处
1)类型检查 2)某些集成可以对const的进行调试
(3)const 与 define 对比
const | #define | |
定义常量 | 定义的常数是变量 有数据类型 | 定义的只是个常数 不带类型 |
起作用的阶段 | 在编译、运行的时候起作用 | 在编译的预处理阶段起作用 |
起作用的方式 | const有对应的数据类型 在编译阶段会执行类型检查 | 只是简单的字符串替换,没有类型检查,仅仅是展开。 define只是简单的字符串替换会导致边界效应: #define N 2+3 //我们预想的N值是5,我们这样使用N
double a = N/2; //我们预想的a的值是2.5,实际上a的值是3.5 |
存储方式 | 分配内存 | 不分配内存 |
空间占用 | 占用数据段空间 给出了对应的内存地址 在程序运行过程中只有一份拷贝 | 占用代码段空间 给出的是立即数 常量在内存中有若干个拷贝 #define PI 3.14 //预处理后占用代码段空间
const float PI=3.14;
//本质上还是一个 float,占用数据段空间 |
代码调试 | 可以进行调试 | 不能进行调试的,因为在预编译阶段就已经替换掉了 |
是否可以再定义 | 不能重定义 | 可以通过#undef取消掉某个符号的定义,再重新定义 |
特殊功能 | 不可 | 可以用来防止头文件重复引用。 //主要把以下代码放在头文件中,可以防止头文件被重复引用
#ifndef xxx //如果没有定义xxx
#define xxx //定义xxx
//这里是你的代码
#endif //结束如果 |
PS: 头文件被重复引用的弊端:
(1) 有些头文件重复引用 只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些,但是对于大工程而言编译效率低下那将是一件多么痛苦的事情。
(2) 有些头文件重复包含,会引起错误,比如在头文件中定义了全局变量,这种会引起重复定义。
四、四种cast转换
(1)const_cast 用法:const →非const
const_cast<type_id> (expression)
该运算符用来修改类型的 const 或 volatile 属性。除了 const 或 volatile 修饰之外, type_id 和 expression 的类型是一样的。常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
(2)static_cast 用法:各种隐式转换
static_cast < type-id > ( expression )
该运算符把 expression 转换为 type-id 类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
-
用于类层次结构中基类和子类之间,指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
-
用于基本数据类型之间的转换,如把 int 转换成 char,把 int 转换成 enum。这种转换的安全性也要开发人员来保证。
-
把空指针转换成目标类型的空指针。
-
把任何类型的表达式转换成void类。
注意: static_cast 不能转换掉 expression 的 const、volitale、或者 __unaligned 属性。
(3)dynamic_cast 动态类型转换
只能用于含有虚函数的类,类层次间向上or向下转化。只能转指针or引用。
允许在运行时刻进行类型转换,从而使程序能够在一个类层次结构安全地转换类型。dynamic_cast 提供了两种转换方式,把基类指针转换成派生类指针,或者把指向基类的左值转换成派生类的引用。
(4)reinterpret_cast 几乎什么都可以转
reinpreter_cast<type-id> (expression)
type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。
为啥不用C的强制转换?
看起来厉害,实际上转化不够明确,不能进行错误检查,容易出错。
五、extern
- extern(外部的)声明变量或函数为外部链接,默认为可被外部使用,即该变量或函数名在其它文件中可见。
- 被其修饰的变量(外部变量)是静态分配空间的,即程序开始时分配,结束时释放。用其声明的变量或函数,应该在别的文件或同一文件的其它地方定义(实现)。
- 在 C++ 中,还可用来指定使用另一语言进行链接,这时需要与特定的转换符一起使用。目前仅支持C转换标记,来支持 C 编译器链接。使用这种情况有两种形式:
extern "C" 声明语句
extern "C" { 声明语句块 }
C++中的extern ‘C’的作用
- extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义
- extern ‘C’修饰的变量和函数,按照C语言编译器的方式来编译和链接
如何引用一个已经定义过的全局变量
- 引用头文件的方式:在编译器就可以发现错误
- extern关键字:在链接期间才报错
参考:菜鸟教程、牛客网C++专题