一、static
当与不同类型一起使用时,static关键字具有不同的含义,而且影响也不同
1. 程序的内存分配
- 静态存储区(全局区):全局变量和静态变量的存储是在静态区,初始化的全局变量和静态变量在一块区域 .data,未初始化的全局变量和静态变量在相邻的另一块区域 .bss。程序结束后由系统释放。(相当于未初始化数据区和已初始化数据区)
- 栈区:由编译器自动分配释放,存放函数的参数值、局部变量等。
- 堆区::一般由程序员分配释放,即动态内存分配。
- 文字常量区:存放常量字符串,程序结束后由系统释放。
- 程序代码区:用于存放程序的二进制代码。
2. 静态变量
2.1. 局部静态变量
当变量声明为static时,空间将在程序的生命周期内分配。即使多次调用该函数,静态变量的空间也只分配一次,前一次调用中的变量值通过下一次函数调用传递。若不加static修饰,函数或者代码块中的变量在函数或者代码块执行完毕后就直接回收销毁了,每次执行都会重新分配内存,每次都会销毁。
- 改变:从自动变量变为静态变量,变量的属性和作用域不受影响
- 作用域:仍为局部作用域,当其所在的函数或者语句块结束的时候,作用域结束。然而局部静态变量离开作用域后,并没有被销毁,仍然驻留在内存中,只有该函数可以对其进行调用或者访问。
输出:void demo() { // static variable static int count = 0; cout << count << " "; count++; } int main() { for (int i=0; i<5; i++) demo(); return 0; }
程序中count被声明为static。每次调用函数时,都不会对变量计数进行初始化。0 1 2 3 4
2.2. 全局静态变量
在全局变量前加上关键字 static,全局变量就变成了一个全局静态变量。
- 改变:当 static 作用于函数定义时,或者用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性。 外部链接属性变为内部链接属性 ,标识符的存储类型和作用域不受影响。也就是说变量或者函数只能在当前源文件中访问,不能在其他源文件中访问。即便是 extern 外部声明也不可以。
- 作用域:全局静态变量在其所声明的文件之外是不可见的,准确的说是从定义处到文件末尾。
2.3. 静态函数
在函数返回类型前加关键字 static,函数就被定义为静态函数,静态函数只在其所声明的文件中可见,不可被其他文件所使用。
- 改变:当 static 作用于函数定义时,或者用于代码块之外的变量声明时,static关键字用于修改标识符的链接属性。 外部链接属性变为内部链接属性 ,标识符的存储类型和作用域不受影响。也就是说变量或者函数只能在当前源文件中访问,不能在其他源文件中访问。
3. 类中的静态成员
3.1. 静态成员变量
声明为static的变量只被初始化一次,它们在单独的静态储存中分配了空间,类中的静态变量由类的所有对象共享 。
- 改变:
- 静态数据成员可以实现多个对象之间的数据共享,它是类的所有对象的共享成员,它在内存中只占一份空间,如果改变它的值,则各对象中这个数据成员的值都被改变。
- 静态数据成员是在程序开始运行时被分配空间,到程序结束之后才释放,只要类中指定了静态数据成员,即使不定义类的对象,也会为静态数据成员分配空间。
- 静态数据成员可以被初始化,但是只能在类体外进行初始化,类中的 static 变量是属于类的,不属于某个对象,它在整个程序的运行过程中只有一个副本,因此不能在定义对象时 对变量进行初始化,就是不能用构造函数进行初始化,
- 静态数据成员既可以通过类对象名引用,也可以通过类名引用。建议使用类名和范围解析运算符调用静态成员
输出:class Apple { public: static int i; Apple(){}; }; int Apple::i = 1;//在类体外进行初始化 int main() { std::cout << Apple::i << std::endl; Apple obj1; Apple obj2; obj1.i = 2; obj2.i = 3; //静态变量由所有对象共享 std::cout << Apple::i << std::endl; std::cout << obj1.i << " " << obj2.i << std::endl; return 0; }
为多个类对象创建静态变量i的多个副本,但这并没有发生。所有类对象共享一个静态变量i1 3 3 3
3.2. 静态成员函数
允许静态成员函数仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。
- 改变:
- 静态成员函数和静态数据成员一样,他们都属于类的静态成员,而不是对象成员。
- 非静态成员函数有 this 指针,而静态成员函数没有 this 指针。
- 静态成员函数主要用来访问静态数据成员而不能访问非静态成员。
二、const
用类型修饰符const修饰的变量或对象的值是不能被更新的
1. const的主要作用
可以定义常量
const int a = 100;
防止修改,起保护作用(函数传参等)
void fun(const int i) { i = 10;//error! }
类型检查
const进行类型检查节省空间,避免不必要的内存分配
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。
2. const的使用
2.1. const修饰普通类型的变量
const int a = 7;
const int i; //error
int b = a; //ok
a = 8; // error,
a为常量,不可更改!i为常量,必须进行初始化!(因为常量在定义后就不能被修改,所以定义时必须初始化。)
2.2. const修饰指针变量
与指针相关的const有四种:
const char * a; //指向const对象的指针或者说指向常量的指针。
char const * a; //同上
char * const a; //指向类型对象的const指针。或者说常指针、const指针。
const char * const a; //指向const对象的const指针。
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
2.2.1. 指针常量(指向常量的指针)
int main()
{
int val = 3;
const int val = 4;
const int * ptr;
*ptr = 10; //error, 指针指向的值不能改
ptr = &val_const; //correct,允许把const对象的地址赋给const对象的指针
ptr = &val; //correct, 指针指向的地址可以改;允许把非const对象的地址赋给指向const对象的指针
int * ptr1 = &val_const; //error,不能把const对象的地址赋给非const对象的指针
int * ptr1 = &val; //利用普通的指针修改(指向常量的指针)指向的值
*ptr1 = 15; //此时这个val对象的地址中保存的值改为15,通过这种方式改变指针常量指向对象的值
std::cout << *ptr << std::endl;
void *pv = &val_const; //error,不能使用void *指针保存const对象的地址
const void * cpv = &val_const; //correct,允许用const void*指针保存const对象的地址
return 0;
}
输出:15
对于指向常量的指针,不能通过指针来修改对象的值。
允许把非const对象的地址赋值给const对象的指针
如果要修改指针所指向的对象值,必须通过其他方式修改(利用普通指针),不能直接通过当前指针直接修改。
不能使用**void * 指针保存const对象的地址,必须使用const void* ** 类型的指针保存const对象的地址。
2.2.2. 常量指针(常指针)
const指针必须进行初始化,且const指针的值不能修改。
int main(){
int val1 = 1;
int val2 = 10;
const int val2_const = 20;
int * const ptr = &val2; //const指针必须初始化!且const指针的值不能修改
ptr = &val1; //error,const指针的值不能修改
*ptr = 2; //correct,const指针指向的值可以修改
int * t = &val2;
*t = 15; //也可以通过非const指针修改ptr所指向的值
int * const p2 = &val2_const; //error,p2指向的是一个变量,而不是const常量
cout<<*ptr<<endl;
}
输出:15
- const指针必须初始化, 且const指针的值不能修改, 但const指针指向的值可以修改
- 如果要修改指针所指向的对象值,可以直接修改,也可以通过其他方式修改(利用普通指针)。
- 当把一个const常量的地址赋值给ptr时候,由于ptr指向的是一个变量,而不是const常量,所以会报错。
2.2.3. 指向常量的常指针
const int p = 3;
const int * const ptr = &p;
2.3. 函数中使用const
2.3.1. const修饰函数参数
- 传递过来的参数及指针本身在函数内不可变,无意义
表明参数在函数体内不能被修改,但此处没有任何意义,var本身就是形参,在函数内不会改变。包括传入的形参是指针也是一样。void func(const int var); // 传递过来的参数不可变 void func(int *const var); // 指针本身不可变
输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const 修饰。 - 参数指针所指内容为常量不可变
其中src 是输入参数,dst 是输出参数。给src加上const修饰后,如果函数体内的语句试图改动src的内容,编译器将指出错误。这就是加了const的作用之一。void StringCopy(char *dst, const char *src);
- 参数为引用,为了增加效率同时防止修改
void func(const A &a)
对于非内部数据类型(自定义class,struct等)的参数而言,像void func(A a) 这样声明的函数注定效率比较低。因为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。
为了提高效率,可以将函数声明改为void func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void func(A &a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void func(const A &a)。
以此类推,是否应将void func(int x) 改写为void func(const int &x),以便提高效率?完全没有必要,因为内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
2.3.2. const修饰函数返回值
跟const修饰普通变量以及指针的含义基本相同:
- const修饰内置类型的返回值,修饰与不修饰返回值作用一样
const int func1();
- const 修饰返回的指针或者引用,是否返回一个指向const的指针,取决于我们想让用户干什么
const int* func2(); //指针指向的内容不变 int *const func2(); //指针本身不可变
2.4. 类中使用const
2.4.1. const成员变量
- 对于类中的const成员变量必须通过初始化列表进行初始化
由于const变量必须初始化,所以类中的const成员变量在声明之后,如果在构造函数中,做的就是对它们赋值,这是不允许的。class Apple { private: int people[100]; public: Apple(int i); const int apple_number; }; Apple::Apple(int i):apple_number(i) //const成员变量必须通过初始化列表进行初始化 {}
2.4.2. const成员函数
const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为const成员函数。只有常成员函数才有资格操作常量或常对象,没有使用const关键字明的成员函数不能用来操作常对象。
- 常成员函数不能修改调用对象的值
class Test { public: Test(){} Test(int _m):_cm(_m){} int get_cm()const //定义为常成员函数,_cm的值就不能改变 { ++_cm; //error, 常成员函数不能修改对象的值 ++_ct; //correct,_ct变量为mutable,可以随时改变 return _cm; } private: int _cm; mutable int _ct; //mutable关键字修饰的成员可以处于不断变化中 }; void Cmf(const Test& _tt) { cout<<_tt.get_cm();//相当于传入的参数不希望被修改 } int main(void) { Test t(8); Cmf(t); return 0; }
- const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
- 可以使用mutable关键字声明成员,使成员可以随时改变,即在const成员函数可以修改它的值
注意:const关键字不能与static关键字同时使用,因为static关键字修饰静态成员函数,静态成员函数不含有this指针,即不能实例化,const成员函数必须具体到某一实例。
3. const进行类型检查
const常量与#define宏定义常量的区别:
- 编译阶段: define 是在编译预处理阶段起作用,const 是在编译阶段和程序运行阶段起作用
- 安全性:define 定义的宏常量没有数据类型,只是进行简单的替换,不会进行类型安全的检查;const 定义的只读变量是有类型的,是要进行判断的,可以避免一些低级的错误
- 内存占用:define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份;const 定义的只读变量在程序运行过程中只有一份
- 调试:define 定义的不能调试,因为在预编译阶段就已经进行替换了;const 定义的可以进行调试
四、volatile
1. volatile
volatile可理解为防止编译器优化,保持内存可见性;即确保本条指令不会因编译器的优化而省略,且要求每次直接读值。使编译器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
#include<iostream>
using namespace std;
int main()
{
const int n = 10;
int p = (int)&n;
*p = 20;
cout<<n<<endl;
return 0;
}
输出:10
解释:
- 在编译期间,编译器可能对代码进行优化
- 当编译器看到此处的n被const修饰,从语义上来讲,n是不期望被修改的
- 所以优化的时候把n的值存放到寄存器中,以提高访问的效率(当变量是“易变的”,编译器一般都不会考虑将变量放入寄存器中,而是内存,而这里的n被看做“不易被改变的”,随编译器考虑将其放入寄存器中进行优化)
- 只要以后使用n的地方都去寄存器中取,即使n在内存中的值发生变化,寄存器也不受影响,所以输出的n的值为10
加上volatile后:
#include<iostream>
using namespace std;
int main()
{
volatile const int n = 10;
int *p = (int*)&n;
*p = 20;
cout<<n<<endl;
return 0;
}
输出:20
2. volatile的应用
2.1.并行设备的硬件寄存器(如状态寄存器)
int *output = (unsigned int *)0xff800000; //定义一个IO端口;
int init(void)
{
int i;
for(i=0;i< 10;i++)
{
*output = i;
}
}
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为 9,所以编译器最后给你编译编译的代码结果相当于:
int init(void)
{
*output = 9;
}
这就违背了对此设备进行顺序赋值的初衷,这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
一个变量可被const和volatile同时修饰(例如一个只读的状态寄存器)
2.2.一个中断服务子程序中访问到的变量
static int i=0;
int main()
{
while(1)
{
if(i) dosomething();
}
}
/* Interrupt service routine */
void IRS()
{
i=1;
}
上面示例程序的本意是产生中断时,由中断服务子程序IRS响应中断,变更程序变量i,使在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远不会被调用。如果将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。
一个指针可被volatile修饰,(当一个中断服务子程序修改一个指向一个buffer的指针时
2.3.多线程应用中被多个任务共享的变量
当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。
五、extern
1. extern基本解释
extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。
2. extern做变量声明
声明extern关键字的全局变量和函数可以使得它们能够跨文件被访问。我们一般把所有的全局变量和全局函数的实现都放在一个*.cpp文件里面,然后用一个同名的*.h文件包含所有的函数和变量的声明。如:
/*Demo.h*/
#pragma once
extern int a;
extern int b;
int add(int a,int b);
/*Demo.cpp*/ //如果将Demo.cpp写成了Demo.c,编译器会告诉你说无法解析的外部符号。因为Demo.c里面的实现会被C编译器处理,然而C++和C编译器在编译函数时存在差异,所以会存在找不到函数的情况。
#include "Demo.h"
int a =10;
int b =20;
int add(intl,intr)
{
return l +r;
}
- 全局函数的声明语句中,关键字extern可以省略,因为全局函数默认是extern类型的。
- 如果在一个文件里定义了char a[] = “123456”;在另外一个文件中必须使用extern chara[ ];来声明。不能使用extern char* a来声明。extern是严格的声明。且extern char* g_str只是声明的一个全局字符指针。
- 指向类型 T 的指针并不等价于类型 T 的数组。extern char *a声明的是一个指针变量而不是字符数组,因此与实际的定义不同,从而造成运行时非法访问。应该将声明改为extern char a[ ]。
- 这提示我们,在使用extern时候要严格对应声明时的格式
3. extern “C”
C++虽然兼容C,但C++文件中函数编译后生成的符号与C语言生成的不同。因为C++支持函数重载,C++函数编译后生成的符号带有函数参数类型的信息,而C则没有。例如int add(int a, int b)函数经过C++编译器生成.o文件后,add会变成形如add_int_int之类的, 而C的话则会是形如_add, 就是说:相同的函数,在C和C++中,编译后生成的符号不同。此时extern “C”就起作用了:告诉链接器去寻找_add这类的C语言符号,而不是经过C++修饰的符号。
- C++调用C函数:
//xx.h extern int add(...) //xx.c int add(){ } //xx.cpp extern "C" { #include "xx.h" }
- C调用C++函数
//xx.h extern "C"{ int add(); } //xx.cpp int add(){ } //xx.c extern int add();
六、inline
1.引入inline关键字的原因
在c/c++中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了inline修饰符,表示为内联函数。
inline函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。
2. inline的使用
定义在类中的成员函数缺省都是内联的,如果在类定义时就在类内给出函数定义,那当然最好。如果在类中未给出成员函数定义,而又想内联该函数的话,那在类外要加上inline,否则就认为不是内联的。且inline一定要与函数定义放在一起,inline是一种“用于实现的关键字,而不是用于声明的关键字”
在头文件中的声明方法:
class A
{
public:
void f1(int x);
void Foo(int x,int y) // 定义即隐式内联函数!
{
};
void f1(int x); // 声明后,要想成为内联函数,必须在定义处加inline关键字。
};
实现文件中定义内联函数:
#include "inline.h"
int Foo(int x,int y); // 函数声明
//inline要起作用,inline要与函数定义放在一起,inline是一种“用于实现的关键字,而不是用于声明的关键字”
inline int Foo(int x,int y) // 函数定义
{
return x+y;
}
// 定义处加inline关键字,推荐这种写法!
inline void A::f1(int x){
}
int main()
{
cout<<Foo(1,2)<<endl;
}
编译器对inline函数的处理步骤:
- 将 inline 函数体复制到 inline 函数调用点处;
- 为所用 inline 函数中的局部变量分配内存空间;
- 将 inline 函数的的输入参数和返回值映射到调用方法的局部变量空间中;
- 如果 inline 函数有多个返回点,将其转变为 inline 函数代码块末尾的分支(使用 GOTO)。
3.inline的使用注意事项
内联能提高函数效率,但并不是所有的函数都定义成内联函数!内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收货会更少!
另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜用内联:
(1)如果函数体内的代码比较长,使得内联将导致内存消耗代价比较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
虚函数(virtual)可以是内联函数(inline)吗?
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译器建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
- inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如 Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
class Base { public: inline virtual void who() { cout << "I am Base\n"; } virtual ~Base() {} }; class Derived : public Base { public: inline void who() // 不写inline时隐式内联 { cout << "I am Derived\n"; } }; int main() { // 此处的虚函数 who(),是通过类(Base)的具体对象(b)来调用的,编译期间就能确定了,所以它可以是内联的,但最终是否内联取决于编译器。 Base b; b.who(); // 此处的虚函数是通过指针调用的,呈现多态性,需要在运行时期间才能确定,所以不能为内联。 Base *ptr = new Derived(); ptr->who(); // 因为Base有虚析构函数(virtual ~Base() {}),所以 delete 时,会先调用派生类(Derived)析构函数,再调用基类(Base)析构函数,防止内存泄漏。 delete ptr; ptr = nullptr; system("pause"); return 0; }
七、friend
1. 友元
友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:
(1)友元函数:普通函数对一个访问某个类中的私有或保护成员。
(2)友元类:类A中的成员函数访问类B中的私有或保护成员。
优点:提高了程序的运行效率。
缺点:破坏了类的封装性和数据的透明性。
2. 友元函数
在类声明的任何区域中声明,而定义则在类的外部。
`friend <类型><友元函数名>(<参数表>);``
友元函数只是一个普通函数,并不是该类的类成员函数,它可以在任何地方调用,友元函数中通过对象名来访问该类的私有或保护成员。
class A
{
public:
A(int _a):a(_a){};
friend int geta(A &ca); // 友元函数
private:
int a;
};
int geta(A &ca)
{
//通过类对象名访问类的私有或保护成员
return ca.a;
}
int main()
{
A a(3);
cout<<geta(a)<<endl;
return 0;
}
输出:3
3. 友元类
友元类的声明在该类的声明中,而实现在该类外。friend class <友元类名>;
类B是类A的友元,那么类B可以直接访问A的私有成员。
class A
{
public:
A(int _a):a(_a){};
friend class B; //友元类
private:
int a;
};
class B
{
public:
//友元类中的方法可以直接访问A的私有成员
int getb(A ca) {
return ca.a;
};
};
int main()
{
A a(3);
B b;
cout<<b.getb(a)<<endl;
return 0;
}
输出:3
注意:
- 友元关系没有继承性,假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员
- 友元关系没有传递性, 假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。
八、explicit
1. 隐式转换类型
class A {
public:
A(int) { }
operator bool() const { return true; }
};
void doA(A a) {}
int main()
{
A a1(1); // OK:直接初始化
A a2 = 1; // OK:复制初始化,此时在编译时默认将1转换为“A temp(1)”类A的对象
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 进行直接初始化
}
函数doA需要的是A类型的参数, 而我们传入的是一个int, 这个程序却能成功运行, 就是因为这隐式调用。 另外说一句, 在对象刚刚定义时, 即使你使用的是赋值操作符=, 也是会调用构造函数, 而不是重载的operator=运算符.
2. explicit关键字
- explicit 修饰构造函数时,可以防止隐式转换和复制初始化
- explicit 修饰转换函数时,可以防止隐式转换,但按语境转换除外
class B { public: explicit B(int) {} explicit operator bool() const { return true; } }; void doB(B b) {} int main() { 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; }
九、decltype
1. decltype基本使用
基本语法:decltype(expression)
注意,decltype 仅仅“查询”表达式的类型,并不会对表达式进行“求值”。
1.1 推导出表达式类型
int i = 4;
decltype(i) a; //推导结果为int。a的类型为int。
1.2 与using/typedef合用,用于定义类型
using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型
using ptrdiff_t = decltype((int*)0 - (int*)0);
using nullptr_t = decltype(nullptr);
vector<int >vec;
typedef decltype(vec.begin()) vectype;
for (vectype i = vec.begin; i != vec.end(); i++)
{
//...
}
1.3 重用匿名类型
struct
{
int d ;
doubel b;
}anon_s;
借助decltype,我们可以重新使用这个匿名的结构体:decltype(anon_s) as ;//定义了一个上面匿名的结构体
1.4 泛型编程中结合auto,用于追踪函数的返回值类型
template <typename T>
auto multiply(T x, T y)->decltype(x*y)
{
return x*y;
}
2.判别规则
- 如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误。
- 否则 ,假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&&
- 否则,假设e的类型是T,如果e是一个左值,那么decltype(e)为T&。
- 否则,假设e的类型是T,则decltype(e)为T。
仅仅为i加上了(),就导致类型推导结果的差异。这是因为,i是一个标记符表达式,根据推导规则1,类型被推导为int。而(i)为一个左值表达式,所以类型被推导为int&。int i=10; decltype(i) a; //a推导为int decltype((i))b=i;//b推导为int&,必须为其初始化,否则编译错误
int i = 4; int arr[5] = { 0 }; int *ptr = arr; struct S{ double d; }s ; void Overloaded(int); void Overloaded(char);//重载的函数 int && RvalRef(); const bool Func(int); //规则一:推导为其类型 decltype (arr) var1; //int 标记符表达式 decltype (ptr) var2;//int * 标记符表达式 decltype(s.d) var3;//doubel 成员访问表达式 //decltype(Overloaded) var4;//重载函数。编译错误。 //规则二:将亡值。推导为类型的右值引用。 decltype (RvalRef()) var5 = 1; //规则三:左值,推导为类型的引用。 decltype ((i))var6 = i; //int& decltype (true ? i : i) var7 = i; //int& 条件表达式返回左值。 decltype (++i) var8 = i; //int& ++i返回i的左值。 decltype(arr[5]) var9 = i;//int&. []操作返回左值 decltype(*ptr)var10 = i;//int& *操作返回左值 decltype("hello")var11 = "hello"; //const char(&)[9] 字符串字面常量为左值,且为const左值。 //规则四:以上都不是,则推导为本类型 decltype(1) var12;//const int decltype(Func(1)) var13=true;//const bool decltype(i++) var14 = i;//int i++返回右值
3.auto和decltype的差别
decltype和auto都可以用来推断类型,但是二者有几处明显的差异:
- auto忽略顶层const,decltype保留顶层const;
- 对引用操作,auto推断出原有类型,decltype推断出引用;
- 对解引用操作,auto推断出原有类型,decltype推断出引用;
- auto推断时会实际执行,decltype不会执行,只做分析。
十、强制类型转换操作
C++中支持的强制类型转换运算符:
- const_cast<type> (expr):const_cast用于修改类型的 const / volatile 属性。除了 const 或 volatile 属性之外,目标类型必须与源类型相同。
- dynamic_cast<type> (expr):dynamic_cast 在运行时执行转换,验证转换的有效性。如果转换未执行,则转换失败,表达式 expr 被判定为 null。dynamic_cast 执行动态转换时,type 必须是类的指针、类的引用或者 void*,如果 type 是类指针类型,那么 expr 也必须是一个指针,如果 type 是一个引用,那么 expr 也必须是一个引用。
- reinterpret_cast<type> (expr):reinterpret_cast 运算符把某种指针改为其他类型的指针。它可以把一个指针转换为一个整数,也可以把一个整数转换为一个指针。
- static_cast<type> (expr):static_cast 运算符执行非动态转换,没有运行时类检查来保证转换的安全性。例如,它可以用来把一个基类指针转换为派生类指针。
1. const_cast
变量本身的const属性是不能去除的,要想修改变量的值,一般是去除指针(或引用)的const属性,再进行间接修改。const_cast 中的type必须是指针、引用或指向对象类型成员的指针
class A
{
public:
virtual void f();
int i;
};
const volatile int* cvip;
int* ip;
void use_of_const_cast()
{
const A a1;
const_cast<A&>(a1).f(); // remove const
ip = const_cast<int*> (cvip); // remove const and volatile
}
2. dynamic_cast
除了dynamic_cast以外的转换,其行为的都是在编译期就得以确定的,转换是否成功,并不依赖被转换的对象
dynamic_cast依赖于RTTI信息,其次,在转换时,dynamic_cast会检查转换的source对象是否真的可以转换成target类型,这种检查不是语法上的,而是真实情况的检查。
若对指针进行dynamic_cast,失败返回null,成功返回正常cast后的对象指针;
若对引用进行dynamic_cast,失败抛出一个异常,成功返回正常cast后的对象引用。
主要作用:将基类的指针或引用安全地转换成派生类的指针或引用,并用派生类的指针或引用调用非虚函数。如果是基类指针或引用调用的是虚函数无需转换就能在运行时调用派生类的虚函数。
注意:
- dynamic_cast在将父类cast到子类时,父类必须要有虚函数,否则编译器会报错。
- dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。
通过代码进行说明:
//定义一个父类和一个子类
class Base
{
public:
virtual void fun()
{
cout<< "Base" << endl;
}
int base_data = 0;
};
class Derived:public Base
{
public:
void fun()
{
cout<< "Derived" << endl;
}
int derived_data = 1;
};
对于下行转换有两种情况,一个父类类型的指针Base *P,但是由于子类继承与父类,父类指针可以指向父类对象,也可以指向子类对象
//父类指针指向子类对象,则P1确实是指向子类对象的
Base *P1 = new Derived();
P1->fun(); //输出Derived
cout << P1->derived_data << endl; //error:Base中没有derived_data成员,说明并未转型
//以下两种转换都能成功
Derived *pd1 = static_cast<Derived *>(P1);
pd1->fun(); //输出Derived
cout << pd1->derived_data << endl; //输出1
Derived *pd2 = dynamic_cast<Derived *>(P1);
pd1->fun(); //输出Derived
cout << pd2->derived_data << endl; //输出1;
//父类指针指向父类对象,则P2是指向父类对象
Base *P2 = new Base;
P2->fun(); //输出Base
//static_cast转型不会报错,且能运行
Derived *pd3 = static_cast<Derived *>(P2);
pd3->fun(); //输出Base,说明转型失败
cout << pd3->derived_data << endl; //输出-1163005939,访问父类中没有的成员,访问越界
//dynamic_cast转型进行安全检查,会报错
Derived *pd4 = dynamic_cast<Derived *>(P2);
pd4->fun(); //error
cout << pd4->derived_data << endl;//error
注:虚函数对于dynamic_cast转换的作用?为何使用dynamic_cast转换类指针时,需要虚函数呢?
Dynamic_cast转换是在运行时进行转换,运行时转换就需要知道类对象的信息(继承关系等)。
如何在运行时获取到这个信息——虚函数表。(RTTI机制)
C++对象模型中,对象实例最前面的就是虚函数表指针,通过这个指针可以获取到该类对象的所有虚函数,包括父类的。因为派生类会继承基类的虚函数表,所以通过这个虚函数表,我们就可以知道该类对象的父类,在转换的时候就可以用来判断对象有无继承关系。
所以虚函数对于正确的基类指针转换为子类指针是非常重要的。
3. reinterpret_cast
允许将任何指针转换为任何其他指针类型。也允许将任何整数类型转换为任何指针类型以及反向转换。reinterpret_cast<type-id>(expression)
主要用途:
- 从指针类型到一个足够大的整数类型
- 从整数类型或者枚举类型到指针类型
- 从一个指向函数的指针到另一个不同类型的指向函数的指针
- 从一个指向对象的指针到另一个不同类型的指向对象的指针
- 从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针
- 从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针
4. static_cast
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性static_cast<type-id>(expression)
主要用途:
- 用于基本数据类型之间的转换,如把int转换为char,把int转换成enum,但这种转换的安全性需要开发者自己保证(这可以理解为保证数据的精度,即程序员能不能保证自己想要的程序安全),如在把int转换为char时,如果char没有足够的比特位来存放int的值(int>127或int<-127时),那么static_cast所做的只是简单的截断,及简单地把int的低8位复制到char的8位中,并直接抛弃高位。
- 把空指针转换成目标类型的空指针
- 把任何类型的表达式类型转换成void类型
- 用于类层次结构中父类和子类之间指针和引用的转换。
十一、this
在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
作用:
- 一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
- this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
使用:
- 在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this。
- 当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
this指针的属性:
- this 指针被隐含地声明为:** ClassName *const this,这意味着不能给 this 指针赋值;在类的const 成员函数中,this 指针的类型为: const ClassName* const,**这说明不能对 this 指针所指向的这种对象是不可修改的(即不能对这种对象的数据成员进行赋值操作);
- this 并不是一个常规变量,而是个右值,所以不能取得 this 的地址(不能 &this)。
持续更新…