★☆ 探讨:临时变量
(一) 类型转换 [ 1. 类型提升、2. 类型截断 3. 强制类型转换 ] 都要生成临时变量
1.1 类型提升
x 类型提升后,就变成无符号了吗? 并没有,对 x 本身并没有什么影响。
1.2 类型截断
都是通过 对ii的临时变量 进行截断,再用这个截断后的临时变量对ch进行赋值。对其原变量没有影响。
1.3 强制类型转换
(二) 传值、传参,传值返回,都要生成临时变量
2.1 传参
形参是实参的一份临时拷贝
2.2 传值返回
都要生成临时变量,都是在对生成的临时变量进行处理,对其本来的变量没有影响
3. 临时变量具有常性,不可修改
(三) 传值返回 和 传引用返回 的性能比较
#include <time.h> struct A{ int a[10000]; }; A a; // 值返回 A TestFunc1() { return a;} // 引用返回 A& TestFunc2(){ return a;} void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; }
通过上述代码的比较,发现 传值和指针在作为传参以及返回值类型上效率相差很大
★ 引用 与 指针 的区别
- 在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间 。
- 在底层实现上实际是有空间的 ,因为 引用是按照指针方式来实现的 。
【 在实际运行显示出来的,还是要以语法为主(虽然其底层实现是靠指针实现): 引用 没开空间;指针 开了空间。】
总结:引用和指针的不同点( 使用 和 概念 上的区别 )
- 引用概念上 定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求(则容易出现 野指针问题)
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个 同类型 实体
- 没有NULL引用,但有NULL指针
[ 3、4点提现:引用更安全,但也不是绝对的安全 ] - 在sizeof 中含义不同:引用 结果为 引用类型的大小,但指针始终是地址空间 所占字节个数(32位平台下占4个字节)
- 引用自加 即 引用的实体增加1,指针自加 即 指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针 需要 显式解引用
*
,引用 编译器自己处理 - 引用 比指针使用起来 相对更安全
八、内联函数
(一)前言:C中的 宏
由于在程序运行时,函数建立栈帧 是一件消耗很大的事情
- 保存寄存器
- 给寄存器开空间
为解决这个问题,C语言推出了宏的概念。
【想了解关于宏的更多知识,目前还没来的及更新,敬请期待,我会将文章链接放在这】
(二)宏的优点
- 增强代码的复用性
- 宏用起来跟函数类似:直接替换,不需要建立栈帧,提高效率
(三)宏的缺点
- 不方便调试宏。(因为 在预编译阶段 就已经进行了替换,调试时是已经编译完了的)
- 要进行替换的内容,离要替换进去的位置是有一定距离的,要检查就没有办法像直接写在程序中来的直观,不能一眼直接检查出代码中出现的问题 。
导致代码可读性差,可维护性差,容易误用。 - 没有类型安全的检查
C++针对宏的缺点作出的改进:用 enum const inline内联函数 替代 宏
(1)enum const 替代 宏常量
(2)inline 替代 宏函数
内联函数
(一)概念
以 inline修饰 的函数叫做 内联函数,编译时C++编译器 会在 调用内联函数的地方展开,没有函数调用 建立栈帧的开销 ,内联函数提升程序运行的效率。
inline修饰后,编译时C++编译器 会在 调用内联函数的地方展开
内联展开 查看方式:
- 右键 <文件名>
- 点击 <属性>
- 在 release模式下,查看编译器生成的汇编代码中是否存在call Add 【 默认条件下,函数还是会建立栈帧 】
- 在 debug模式下,需要对编译器进行设置,否则不会展开
(因为debug模式下,编译器默认不会对代码进行优化,以下给出vs2013的设置方式)
(二)特性
- inline是一种以 空间换时间 的做法,如果 编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。
- 缺陷:可能会使 目标文件变大
- 优势:不用建立栈帧,提高程序运行效率
- inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同。
- 一般建议:
- 将函数规模较小( 即函数不是很长,具体没有准确的说法,取决于编译器内部实现,大概10行以内 )、不是递归、且 频繁调用 的函数采用inline修饰,
- 否则编译器会忽略inline特性。
下图为《C++prime》第五版关于inline的建议:
- inline不建议声明和定义分离,分离会导致链接错误。
因为 inline被展开(直接在程序中展开,就不需要地址了),就没有函数地址了,链接就会找不到。
//声明和定义分离 // F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdeclf(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
总结内联函数inline 优点:
- inline + 函数 ——> 内联函数 [ 好写,语法简单 ]
- 效率和宏一样:不需要进建立栈帧(会展开(编译器会将函数的逻辑调出来))
- 还能调试
对于宏的缺点,内联函数inline 都能克服。
九、auto 关键字(C++11)
前言:类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:
- 类型难于拼写
- 含义不明确导致容易出错
typeid 打印类型
#include <string> #include <map> int main() { std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange", "橙子" }, {"pear","梨"} }; std::map<std::string, std::string>::iterator it = m.begin(); while (it != m.end()) { //.... } return 0; }
auto 对于像int 3个字符,auto 4个字符,差别不大,没有任何意义。
- auto主要的用处还得是 在定义对象时,类型较长,用它比较方便:
std::map<std::string, std::string>::iterator
是一个类型,但是该类型太长了,特别容易写错。
typedef 给类型取别名
聪明的同学可能已经想到:可以通过typedef给类型取别名,比如:
#include <string> #include <map> typedef std::map<std::string, std::string> Map; int main() { Map m{ { "apple", "苹果" },{ "orange", "橙子" }, {"pear","梨"} }; Map::iterator it = m.begin(); while (it != m.end()) { //.... } return 0; }
使用typedef给类型取别名确实可以简化代码,但是typedef有会遇到新的难题:
typedef char* pstring; int main() { const pstring p1; // 编译成功还是失败? const pstring* p2; // 编译成功还是失败? return 0; }
在编程时,常常需要把表达式的值赋值给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并非那么容易,因此C++11给auto赋予了新的含义。
(一)auto简介
- 在 早期 C/C++中auto 的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,大家可思考下为什么?
- C++11中,标准委员会赋予了auto全新的含义即:auto不再是一个存储类型指示符 ,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
- 因此,使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。
- 【因此,auto并非是一种“类型”的声明,而是 一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。】
int TestAuto() { return 10; } int main() { int a = 10; auto b = a; auto c = 'a'; auto d = TestAuto(); cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; cout << typeid(d).name() << endl; //auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化 return 0; }
(二)auto的使用细则
- auto与指针
用auto声明指针类型时,用auto和auto*没有任何区别 - auto与引用
auto声明引用类型时则必须加&
int main() { int x = 10; auto a = &x; auto* b = &x; auto& c = x; cout << typeid(a).name() << endl; cout << typeid(b).name() << endl; cout << typeid(c).name() << endl; *a = 20; *b = 30; c = 40; return 0; }
- 同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错。【因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。】
void TestAuto() { auto a = 1, b = 2; auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同 }
(三)auto 不能推导的场景
- auto 不能作为函数的参数 ,函数返回值也不支持
此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导【其原因 与auto初始化一样】。要是函数不进行调用了,那么这个形参auto a;就相当于没有被初始化了
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导。 //要是函数不进行调用了,那么这个形参auto a;就相当于没有被初始化了 void TestAuto(auto a) X {} int main(){ //TestAuto(5); }
- 函数返回值也不支持
X auto TestAuto(auto a) {} int main(){ //TestAuto(5); }
这是非常坑的。
- auto 不能直接用来声明数组
void TestAuto() { int a[] = {1,2,3}; auto b[] = {4,5,6}; }
- 为了避免与C++98中的auto发生混淆,C++11只保留了auto作为 类型指示符 的用法
- auto在实际中最常见的优势用法 就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
十、基于范围的for循环(C++11)
前言
在C++98中如果要遍历一个数组,可以按照以下方式进行:
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i) array[i] *= 2; for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p) cout << *p << endl; }
对于一个 有范围的集合 而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此C++11中引入了基于范围的for循环 。
(一)范围for的语法
- for循环后的括号由 冒号“
:
” 分为两部分 :第一部分是范围内用于 迭代的变量,第二部分则表示 被迭代的范围 。
void TestFor() { int array[] = { 1, 2, 3, 4, 5 }; for(auto& e : array) e *= 2; for(auto e : array) cout << e << " "; return 0; }
【 注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。 】
(二)语法的含义
依次取数组中的数值赋值给e,自动判断结束,自动++往后走。
(三)范围for的使用条件
- for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定
void TestFor(int array[]) { //用auto:要是数组类型变了,这里也不用动类型 for(auto& e : array) //这里,auto& 即可改变数组array (指针无法替代) cout<< e <<endl; //也可以用实际类型 for(int& e : array) cout<< e <<endl; }
这里,auto& 即可改变数组array (指针无法替代)。 只有&引用能做到,指针不能。
- 迭代的对象要实现++和==的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在大家了解一下就可以了)
十一、指针空值nullptr(C++11)
前言:C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr() { int* p1 = NULL; int* p2 = 0; // …… }
- NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:
#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
可以看到, NULL可能被定义为 字面常量0,或者被定义为 无类型指针( void * )的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,比如:
void f(int) { cout<<"f(int)"<<endl; } void f(int*) { cout<<"f(int*)"<<endl; } int main() { //程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。 //在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量。 f(0); f(NULL); //如果要将其按照指针方式来使用,必须对其进行强转(void*)0。 f((int*)NULL); return 0; }
程序本意是想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,因此与程序的初衷相悖。
(一)指针空值nullptr (C++11)
- 在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void * )常量,但是编译器默认情况下将其看成是一个整形常量。
- C++为了弥补C:NULL的不足,定义出nullptr 。
nullptr
- 注意:
- 在使用nullptr表示指针空值时,不需要包含头文件,因为 nullptr 是 C++11作为新关键字 引入的。
- 在C++11中,sizeof(nullptr) 与 sizeof((void * )0)所占的字节数相同 。
- 为了提高代码的健壮性,在后续 表示指针空值时建议最好使用nullptr。