目录
1.explicit关键字
2.左值和右值概念
3.函数返回当引用
4.C++11 _array容器用法
5.C++类型转换简介
5.1.static_cas转换
5.2.reinterpreter_cast 转换
5.3.dynamic_cast转换
5.4.const_cast 转换
1.explicit关键字
explicit /ɪkˈsplɪsɪt/ 明确的;清楚的;直率的;详述的
作用是表明该构造函数是显示的, 而非隐式的。不能进行隐式转换!跟它相对应的另一个关键字是implicit,意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。
例如我们定义一个类
class student {
public:
student(int age) {
this->age = age;
cout << "age=" << age << endl;
}
student(int age, string name) {
this->age = age;
this->name = name;
cout << "age=" << age << " name=" << name << endl;
}
~student() {
}
int getAge() {
return this->age;
}
string getName() {
return this->name;
}
private:
string name;
int age;
};
在主函数中分别使用显示构造和隐式构造
int main() {
//显示构造,直接在括号里面写
student xiaoM(18);
//显示构造
student xiaoH(19, "小花");
//隐式构造
student xiaoW = 18;
//隐式构造(C++11前编译器无法通过)
student xiaoZ = { 19,"小张" };
system("pause");
return 0;
}
执行结果
当给其中一个构造函数加上explicit关键字后
class student {
public:
explicit student(int age) {
this->age = age;
cout << "age=" << age << endl;
}
private:
string name;
int age;
};
这种声明就表示:这个构造函数只能显示构造,无法进行隐式转换
explicit关键字的作用在于:在实际开发中 等号会产生一些歧义,等号本身又有赋值的作用,无法在一瞬间辨别等号是用于赋值还是调用隐式构造,不便于阅读。
2.左值和右值概念
我们在日常编译程序时,有时可能会遇到如下报错提示:
源代码
#include<iostream>
using namespace std;
int demo() {
int i = 0;
return i;
}
int main() {
//将函数返回作为左值
demo() = 888;
return 0;
}
这里将函数返回赋予888,编译器给出“表达式必须是可修改的左值”的报错提示 ,如果我们将函数返回作为右值,则不会有此报错~
关于左值和右值的问题,这里就涉及到计算机的存储层次结构
存储层次结构
例如我们计算 c = a + b ;
a 和 b 的值会保存在内存中,当程序遇到c = a + b,会将a和b的值拿到cpu的寄存器中,再将计算结果返回给内存中的c,当我们Ctrl + S后,内存中的数据就可以永久保存到磁盘中。
左值和右值的概念
按字面意思,通俗地说。以赋值符号 = 为界,等号(=)左边的就是左值(lvalue),等号(=) 右边就是右值(rvalue)。
lvalue —— 代表一个在内存中占有确定位置的对象(换句话说就是有一个地址)。
rvalue —— 通过排他性来定义,每个表达式不是lvalue就是rvalue。因此从上面的 lvalue的定义,rvalue是在不在内存中占有确定位置的表达式,而是存在在寄存器中。
所有的左值(无论是数组,函数或不完全类型)都可以转换成右值。
我们将下面程序转为汇编码观察
#include<iostream>
using namespace std;
int main() {
int a = 1;
int b = 2;
int c = 3;
c = a + b;
printf("%d", c);
return 0;
}
汇编代码
程序将a、b、c的值都存储在栈上,当执行 c= a + b 时,程序会将 a 和 b 的值拿到寄存器中计算,再将计算结果返回给内存中 c 的位置上去。
也就是说,程序执行的方式,是从内存中取值放到寄存器中,再将计算结果返回给内存中的某个位置!
a + b的结果保存在寄存器中,再将寄存器的值返回给内存
因此,对于 c = a + b;是合法的
而 a + b = c;也就是eax = c就是非法,左值需要在内存中有位置,而a + b是在寄存器 中
3.函数返回当引用
上面我们讲了左值与右值,当我们把函数返回值作为左值时,编译器给出报错提示,而C++11的新增特性“函数返回引用”又可以实现函数返回值作为左值的操作
C++引用使用时的难点:
1.当函数返回参数是引用时
若返回栈变量(自动变量/临时变量),不能成为其它引用的初始值,不能作为左值使用。
2. 若返回静态变量或全局变量
·可以成为其他引用的初始值
·即可作为右值使用,也可作为左值使用
3. 返回形参当引用
(注:C++链式编程中,经常用到引用,运算符重载专题) 例如在重载“<<"左移运算符后,我们想要链式输出,就是返回的cout引用
下面,对第一种种情况分别通过代码举例 👇
1. 函数返回值为引用,返回临时变量作为其他引用的初始值
#include<iostream>
using namespace std;
int& demo() {
int i = 600;
return i; //返回局部变量
}
int main() {
int &a =demo(); //函数引用成为其他引用的初始值
cout << a << endl;
system("pause");
return 0;
}
编译结果
编译器并未报错,也只是给出警告 “ 返回局部变量 ” 。好像目测也没啥问题 ,下面通过一段代码示例,来解释这种操作的不合理性。👇看操作。
我们知道,通过引用可以将两个变量都有同一块地址
因此,对于上面的代码,a 的地址和局部变量 i 有相同的地址
我们现在更改a的值,然后再调用demo函数
int main() {
int &a = demo();
cout << "a的地址为:" <<&a<<endl;
a = 888;
demo();
cout << "再次调用demo(),a的值为:" << a << endl;
system("pause");
return 0;
}
运行结果:
第一次demo函数执行,i 的地址为00D3F854,a的地址也为00D3F854。
然后demo函数执行完毕后,在栈内的空间释放。
第二次demo函数也是在栈的相同位置存储,所以 i 的位置没有改变,因此 i =600后也影响了a的值。
如果我们在栈中使用两个函数,会是什么结果呢?
#include<iostream>
#include<fstream>
using namespace std;
int demo1() {
int i = 100;
cout << "demo1()函数中,i的地址为:" << &i << endl;
return 0;
}
int& demo() {
int i = 600;
cout << "i的地址为:" << &i << endl;
return i; //返回局部变量
}
int main() {
int &a = demo();
cout << "a的地址为:" <<&a<<endl;
a = 888;
demo1();
cout << "调用了demo1(),a的值为:" << a << endl;
system("pause");
return 0;
}
执行结果
也就是说,如果用函数返回引用,返回局部变量和引用初始化,程序会继续认为这个地址还是属于计算机的,并非是程序员自己申请的。
2.返回局部变量作为左值
情况与上面类似
#include<iostream>
#include<fstream>
using namespace std;
int demo1() {
int i = 0;
cout << "demo1()函数中,i的地址为:" << &i << endl;
return 0;
}
int& demo(int **p) {
int i = 600;
*p = &i;
cout << "i的地址为:" << &i << " i的值为:" << i << endl;
return i; //返回局部变量
}
int main() {
int* p = NULL;
demo(&p) = 888;
cout << "p的地址为:" << p << " p的值为:" << *p << endl;
demo1();
cout << "p的地址为:" << p << " p的值为:" << *p << endl;
system("pause");
return 0;
}
执行结果
总结:若返回栈变量(自动变量/临时变量),不能成为其它引用的初始值,不能作为左值使用,虽然编译器只会警告,但在实际开发中是非常危险的操作!
3.例如返回static或者全局变量就不出出现这种情况
#include<iostream>
#include<fstream>
using namespace std;
int& demo() {
static int i = 600;
cout << "i的地址为:" << &i << endl;
return i; //返回静态变量
}
int main() {
int a = demo();
a = 888;
cout << "调用了demo(),a的值为:" << a << endl;
demo();
cout << "再次调用了demo(),a的值为:" << a << endl;
system("pause");
return 0;
}
执行结果
其实,主要还是看返回值的生命周期,如果是立刻销毁的,则应注意使用~
4.C++11 _array容器用法
array容器概念
- array 容器是 C++ 11 标准中新增的序列容器,简单地理解,它就是在 C++ 普通数组的基础上,添加了一些成员函数和全局函数。
- array是将元素置于一个固定数组中加以管理的容器。
- array可以随机存取元素,支持索引值直接存取, 用[]操作符或at()方法对元素进行操作,也可以使用迭代器访问
- 不支持动态的新增删除操作
- array可以完全替代C语言中的数组,使操作数组元素更加安全!
- #include <array>
array特点
- array 容器的大小是固定的,无法动态的扩展或收缩,这也就意味着,在使用该容器的过程无法增加或移除元素而改变其大小,它只允许访问或者替换存储的元素。
- STL 还提供有可动态扩展或收缩存储空间的 vector 容器
array对象的构造
array采用模板类实现,array对象的默认构造形式
array<T,size> arrayT; //T为存储的类型, 为数值型模板参数
//构造函数
#include<array> array<int, 5> a1; //一个存放5个int的array容器 array<float, 6> a2; //一个存放6个float的array容器 array<student, 7> a3; //一个存放7个student的array容器
array的赋值
array 的赋值
a1.assign(0); //玩法一 改变array中所有元素(注:将被废弃,不推荐使用)
在目前版本,编译器会报错
如果非要使用assign的话可以根据报错提示加一个宏
#define _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING 1
a1.fill(666); //玩法二 用特定值填充array中所有元素
array<int, 4> test={1, 2, 3, 4};// 玩法三 定义时使用初始化列表,列表内的数量不能超过定义的大小
array<int, 4> test;
test={1,2,3,4}; //玩法四 定义后使用列表重新赋值
array<int, 4> a1,a2;
a1={1,2,3,4};
a2 = a1;//玩法五,赋值运算
a1.swap(a2); //玩法六 和其它array进行交换
array的大小
array.size(); 返回容器中元素的个数 array.empty(); 判断容器是否为空,逗你玩的,因为容器是固定大小,永远为 false array.max_size(); 返回容器中最大元素的个数,数组是固定大小,这也是逗你玩的同size()。
array的数据存取
第一 使用下标操作 a1[0] = 100;
第二 使用at 方法 如: a1.at(2) = 100;
第三 接口返回的引用 a1.front() 和 a1.back()
注意: 第一和第二种方式必须注意越界
array 迭代器访问
#include<array> using namespace std; array.begin(); 返回容器中第一个数据的迭代器。 array.end(); 返回容器中最后一个数据之后的迭代器。 array.rbegin(); 返回容器中倒数第一个元素的迭代器。 array.rend(); 返回容器中倒数最后一个元素的后面的迭代器。 array.cbegin(); 返回容器中第一个数据的常量迭代器。 array.cend(); 返回容器中最后一个数据之后的常量迭代器。 array.crbegin(); 返回容器中倒数第一个元素的常量迭代器。 array.crend(); 返回容器中倒数最后一个元素的后面的常量迭代器。
#include<iostream> #include<array> using namespace std; int main() { array<int, 5> arrayInt = { 1,2,3,4,5 }; 普通迭代器 for (array<int, 5>::iterator ite = arrayInt.begin(); ite != arrayInt.end(); ite++) cout << *ite << " "; 常量迭代器,常量迭代器无法修改 array<int, 5>::const_iterator ite = arrayInt.cbegin(); 逆向迭代器,逆向迭代器也是++ for (array<int, 5>::reverse_iterator ite = arrayInt.rbegin(); ite != arrayInt.rend(); ite++) cout << *ite << " "; }
set.rbegin()与set.rend()。略。
5.C++类型转换简介
旧式转型 C风格的强制类型
type b = ( type ) a
例如:
int i = 48;
char c = (char ) i;
double PI = 3.1415926; int i = PI; 隐式转换 int i1 = (int) PI ; 强制类型转换 强制类型转换,整数直接变指针 int * addr = (int*) 0x888888; void *p; int * int_arg = (int*) p; 强制转换
新式转型C++风格的类型转换提供了4种类型转换操作符来应对不同场合的应用。
格式:
type b = 类型操作符<type> ( a )
类型操作符= static_cast | reinterpreter_cast | dynamic_cast | const_cast
5.1.static_cas转换
静态类型转换(斯文的劝导,温柔的转换)(编译器会检查转换是否合理)。如int转换成char
char a = 'A'; int b = static_cast <int>(a);
主要用法:
- 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。上行指针或引用(派生类到基类)转换安全,下行不安全(上行可以,下行不安全)
- 用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
- 把空指针转换成目标类型的空指针。
- 把任何类型的表达式转换成void类型
1.用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换
class Animal {
public:
virtual void cry() = 0;
};
class dog :public Animal {
public:
void cry() {
cout << "小狗汪汪汪" << endl;
}
};
int main() {
dog* d = new dog();
Animal* animal = d;//子类对象赋给基类
使用static_cast指针转换
Animal* al = static_cast<Animal*>(d);
引用转换
dog d2;
Animal& a2 = static_cast<Animal&>(d2);
system("pause");
return 0;
}
那为什么说上行转换可以,下行转换不安全呢?
我们再增加一个Animal的派生类
class cat :public Animal {
public:
void cry() {
cout << "小猫喵喵喵" << endl;
}
};
int main() {
dog* d = new dog();
//上行转换安全
Animal* animal = static_cast<Animal*>(d);
//下行转换,这种也是安全的
d = static_cast<dog*>(animal);
//animal已经是dog类转过来的,再转成cat的就不安全的
cat* c;
c = static_cast<cat*>(animal);
system("pause");
return 0;
}
因此,并不是说下行转换不可用,例如上面的将从dog类转过来的animal再转到cat就是不安全的,还是取决于程序员的使用 。
2.基本数据类型之间的转换
int a = 10;
char b = static_cast<char>(a);
3.把空指针转换成目标类型的空指针。
int* p = static_cast<int*>(NULL);
NULL在C++里定义的是void*0
4.把任何类型的表达式转换成void类型
int* p = new int[10];
void* vp = static_cast<void*>(p);
总结:使用static_cast可以使编译器做一些合法性检查,不写也没关系。
5.2.reinterpreter_cast 转换
重新解释类型(挂羊头,卖狗肉) 不同类型间的互转,数值与指针间的互转
用法:
type b = reinterpret_cast <type > ( a )
type 必须是一个指针、引用、算术类型、函数指针。
例如强制将整数转为指针 int* addr = reinterpret_cast<int*>(0x88888); 如果使用static_cast则会报错 int* addr1 = static_cast<int*>(0x88888);
忠告:滥用 reinterpret_cast 运算符可能很容易带来风险。 除非所需转换本身是低级别的,否则应使用其他强制转换运算符之一。
用法1,数值与指针之间转换
int* p = reinterpret_cast<int*>(0x88888);
int val = reinterpret_cast<int>(p);
用法2, 不同类型指针和引用之间的转换
定义一个父类和两个派生类
class Animal {
public:
void cry() {
cout << "动物叫" << endl;
}
};
class cat :public Animal {
public:
void cry() {
cout << "小猫喵喵喵" << endl;
}
};
class dog :public Animal {
public:
void cry() {
cout << "小狗汪汪汪" << endl;
}
};
1.隐式上行 转换 与强制上行 转换
dog d1;
Animal* animal = &d1;
animal->cry();
//如果能用static_cast强转,static_cast优先
dog* d2 = reinterpret_cast<dog*>(animal);
dog* d3 = static_cast<dog*>(animal);
2.不同类型指针转换不能用static_cast
3.但是可以使用reinterpre_cast转换
int main() {
dog d1;
cat* c1 = reinterpret_cast<cat*>(&d1);
c1->cry();
system("pause");
return 0;
}
编译器不管程序是将什么类型转的,只晓得最后要获得一个cat类型,然后按cat的存储类型,访问里面的方法。如果滥用reinterpreter_cast,类型之间转来转去,可能会导致程序快速崩溃。
4.引用的强制类型转换
dog d1;
Animal& animal = d1;
dog& d2 = reinterpret_cast<dog&>(animal);
reinterpreter_cast 再加一个 static_cast 就已经可以替换C语言的转换
5.3.dynamic_cast转换
动态类型转换
- 将一个基类对象指针cast到继承类指针,dynamic_cast 会根据基类指针是否真正指向继承类指针来做相应处理。失败返回null,成功返回正常cast后的对象指针;
- 将一个基类对象引用cast 继承类对象,dynamic_cast 会根据基类对象是否真正属于继承类来做相应处理。失败抛出异常bad_cast
注意:dynamic_cast在将父类cast到子类时,父类必须要有虚函数一起玩。
第一种情况 示例代码
#include<iostream>
using namespace std;
class Animal {
public:
//父类必须有虚函数
virtual void cry() = 0;
};
class cat :public Animal {
public:
void cry() {
cout << "小猫喵喵喵" << endl;
}
};
class dog :public Animal {
public:
void cry() {
cout << "小狗汪汪汪" << endl;
}
};
int main() {
dog d1;
Animal* animal = &d1;
cat* ca= dynamic_cast<cat*>(animal);
if (ca==NULL) {
cout << "这是只狗!" << endl;
}
else {
cout << "这是只猫" << endl;
}
system("pause");
return 0;
}
运行结果
第二种情况示例代码
int main() {
dog d1;
Animal& animal = d1;
dog& d2 = dynamic_cast<dog&>(animal);
system("pause");
return 0;
}
这种是没有任何问题,因为dog与animal是子类与父类的关系,并且animal指针真正指向dog。
但是,如果我们用其他类型接受又会是什么情况呢?
int main() {
dog d1;
Animal& animal = d1;
cat& d2 = dynamic_cast<cat&>(animal);
system("pause");
return 0;
}
基类指针并没有指向cat,这种情况编译是没有任何问题,因为编译器会认为这是父类与子类之间的转换,但是当我们运行时,编译器就会抛出异常
对于抛出异常,我们就可以利用try—catch语句捕获异常,保证程序能继续运行
int main() {
dog d1;
Animal& animal = d1;
try {
cat& d2 = dynamic_cast<cat&>(animal);
}
catch (std::bad_cast bc) {
cout << "不是猫,应该是狗" << endl;
}
system("pause");
return 0;
}
运行结果
5.4.const_cast 转换
去const属性。(仅针对于指针和引用)
例如
#include<iostream>
using namespace std;
void demo(const char* p) {
char* p1 = const_cast<char*>(p);
p1[0] = 'A';
}
int main() {
//字符数组
char p[] = "1234567";
demo(p);
cout << p << endl;
system("pause");
return 0;
}
运行结果
或者直接使用const_cast,运行结果也是一样
int main() {
char p[] = "1234567";
const_cast<char*>(p)[0] = 'A';
cout << p << endl;
system("pause");
return 0;
}
但是,对于常量字符串不能去掉const修改
例如
#include<iostream>
using namespace std;
void demo(const char* p) {
char* p1 = const_cast<char*>(p);
p1[0] = 'A';
}
int main() {
const char *p = "1234567";
demo(p);
cout << p << endl;
system("pause");
return 0;
}
可以编译,但是运行会报错
因为常量所处位置是常量区,内存无法访问常量区。
5.2 类型转换使用建议
1)static_cast静态类型转换,编译的时c++编译器会做编译时的类型检查;隐式转换;
基本类型转换,父子类之间合理转换
2)若不同类型之间,进行强制类型转换,用reinterpret_cast<>() 进行重新解释
建 议:
C语言中 能隐式类型转换的,在c++中可用 static_cast<>()进行类型转换。因C++编译器在编译检查一般都能通过;C语言中不能隐式类型转换的,在c++中可以用 reinterpret_cast<>() 进行强制类型解释。
总结:static_cast<>()和reinterpret_cast<>() 基本上把C语言中的 强制类型转换给覆盖,注意reinterpret_cast<>()很难保证移植性。
3)dynamic_cast<>(),动态类型转换,安全的虚基类和子类之间转换;运行时类型检查
4)const_cast<>(),去除变量的只读属性
最后的忠告:程序员必须清楚的知道: 要转的变量,类型转换前是什么类型,类型转换 后是什么类型,转换后有什么后果。
C++大牛建议:一般情况下,不建议进行类型转换;避免进行类型转换。