编译环境:VS 2013
文章目录
所谓眼见为实,耳听为虚,然鹅,你可能也会被事情的表面蒙蔽了双眼,不信你看下面的代码:
#include <iostream>
#include <cstdlib>
int main(void)
{
const int a = 10;
int * pa = (int *)&a;
*pa = 100;
std::cout << a << std::endl;
std::cout << *pa << std::endl;
system("pause");
return 0;
}
嗯…首先说明一下,这段代码编译没有问题,那么会输出什么呢?
A: 10 10
B: 10 100
C: 100 10
D: 100 100
好,排列组合了一下,选一个吧!
A? B? C? D?
接下来,就是见证奇迹的时刻:
答案是 B 选项,为什么呢?
一、宏和 const
我们先回顾一下 C 语言中的宏定义和宏函数
1. 宏定义和宏函数
我们说,如果某个常量在代码中多次出现,并且经常修改,那我们可以考虑在代码中定义一个宏,这样每次更换的时候,只需要更换这个宏的值,其余地方就跟着替换了,那这是一个好方法啊,宏的定义方式如下:
#define DEFINE_NAME DEFINE_VALUE
例如: #define MAX_SIZE 1024
当然对一些常用的简单函数也是可以把它定义为宏函数的
例如:#define ADD(left, right) ((left) + (right))
宏在代码预处理阶段,进行全文替换,代码中凡是有该宏的地方,全部替换为该宏的值。
那这样做的好处:
- 提高了代码的复用性
- 提高性能
为什么说提高了性能呢?我们知道,调用一个函数是要压栈,形成该函数的栈帧结构的,这肯定是要花费时间的,是有开销的,这也是为什么递归函数运行很慢的原因,而宏只是在代码中进行了替换,就不会有函数调用和压栈开销了,自然就提高了性能。
但这样做也有坏处:
- 没有参数检验
- 没法调试
- 代码膨胀
- 副作用
其他都好理解,这个副作用怎么理解呢?
我们看看下面一段代码:
#include <iostream>
#include <cstdlib>
#define CMP(left, right) (((left) > (right)) ? (left) : (right))
int main(void)
{
int a = 20;
int b = 10;
int c = 0;
c = CMP(++a, b);
std::cout << a << std::endl;
system("pause");
return 0;
}
这段代码输出什么呢?
10? 20? 21?
我们来看看结果:
22?如果你大跌眼镜了,就说明你没有理解我们刚刚说的宏替换
其实代码已经变成这样了
c = (((++a) > (b)) ? (++a) : (b))
自增运算执行了两次,自然就是 22 了。
这就是宏的副作用,加再多括号都避免不了的。
2. C 语言中的 const 关键字
我们说,我们不想修改某个变量的值,可以用const 来修饰这个变量,使这个变量具有常属性。
如 const int a = 10;
这样我们就无法直接修改变量 a 的值了;
但是我们却可以通过指针的方式间接修改 a 的值:
int *pa = &a;
*pa = 20;
这样就把 a 的值修改为 20 了,所以C语言中的 const 关键字是个假的。
二、C++ 中的 const 关键字和内联函数
1. C++ 中的 const
其实C语言中的宏是一个非常不错的想法,加上 C++ 完全兼容 C 的特性,于是我们在 C++ 语言中对 const 有了新的含义,那就是负载了宏的特性,在C++中,const 修饰的变量就是一个常量,并且具有宏的特性。
我们回头再看看篇首的代码…
其实是程序在编译期间,把所有出现 a 的地方替换为 10 了,第一个输出的就是10,我们通过指针的方式,还是间接修改了变量 a 所占地址空间的内容,也就是说 a 变量地址空间里存的是 100,而不是 10 了。我们替换的早,a 打印出来是 10,修改地址空间里的值是在程序运行期间修改的,*pa 打印出来的就是 100 了。
2. 内联函数
建议内联!建议内联!!建议内联!!!
我们说 C 语言在用宏函数的时候直接替换,不进行参数检验,这使得一个非常不错的想法有了隐患,调用者就要考虑副作用了,这无疑会让人脑壳疼。
于是我们在 C++ 中引入了内联函数的概念:
- 有函数的结构,却不具备函数的性质
- 不是在调用时发生控制转移
而是在编译时将函数体嵌入在每一个调用处
类似于宏替换,使函数体替换调用处的函数名- 用关键字inline修饰
- 能否形成内联函数,需要## 标题看编译器对该函数定义的具体处理
所以说,我们只是建议内联,给编译器提个建议,建议把这个函数设置为内联函数,所以不是加了 inline 关键字的函数都能成为内联函数。- 内联扩展是用来消除函数调用时的时间开销
- 是一种用空间换时间的做法
所以代码很长或者有循环/递归的函数不适宜作为内联函数
三、C++11 新规定的 auto 关键字
1. C++11 标准出来之前
C/C++中 auto 关键字的含义是:使用 auto 修饰的变量,是具有自动存储器的局部变量,但遗憾的是一直没有人去使用它,为什么呢?我们想,局部变量开辟内存是在栈上,函数调用结束,栈自动销毁,那我还要这鸡肋的关键字干哈?我又不傻!!
2. C++11 中
标准委员会赋予 auto 全新的含义:
auto 声明的变量必须由编译器在编译时期推导而得
什么意思呢?
就是说定义一个变量,不用给出明确地类型,auto 就像一个“占位符”一样,编译器在编译阶段会根据需要初始化表达式来推导 auto 的实际类型。
#include <iostream>
#include <cstdlib>
int TestAuto()
{
return 0;
}
int main(void)
{
int a = 10;
auto b = 10;
auto c = a;
auto ch = 'c';
auto d = 3.14;
auto ret = TestAuto();
std::cout << "The type of a is "<< typeid(a).name() << std::endl;
std::cout << "The type of b is " << typeid(b).name() << std::endl;
std::cout << "The type of c is " << typeid(c).name() << std::endl;
std::cout << "The type of ch is " << typeid(ch).name() << std::endl;
std::cout << "The type of d is " << typeid(d).name() << std::endl;
std::cout << "The type of ret is " << typeid(ret).name() << std::endl;
system("pause");
return 0;
}
编译执行:
我们通过 typeid().name() 打印出来了变量的类型
看来这个 auto 确实可以推导出变量的类型
下面我们再来看一下 auto 更多的使用规则:
3. auto 的使用规则
- auto 与指针和引用结合起来使用
用 auto 声明指针类型时,用 auto 和 auto* 没有任何区别
但是!!!用 auto 声明引用类型时则必须加 &
#include <iostream>
#include <cstdlib>
int main(void)
{
int x = 10;
auto a = &x;
auto *b = &x;
auto &c = x;
std::cout << "The type of a is "<< typeid(a).name() << std::endl;
std::cout << "The type of b is " << typeid(b).name() << std::endl;
std::cout << "The type of c is " << typeid(c).name() << std::endl;
*a = 20;
std::cout << "x = " << x << std::endl;
*b = 30;
std::cout << "x = " << x << std::endl;
c = 40;
std::cout << "x = " << x << std::endl;
system("pause");
return 0;
}
编译执行:
三者比较刚好证实了我们的结论!
- 在同一行定义多个变量
这些变量必须是同类型的,否则编译器报错
编译器只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
4. auto 不能推导的场景
- auto 不能作为函数的参数
- auto 不能直接用来声明数组
好,我们了解了 auto 这个关键字,然后发现,它还是个鸡肋!!!
要是 auto 只有这么点功能,标准委员会也不会给他重新赋予新含义了
它的魅力在于:
四、C++11 的基于范围for循环
1. 范围 for 的语法
for (declaration : expression)
statement
- expression 部分
是一个对象,用于表示一个序列- declaration
负责定义一个变量,该变量将被用于访问序列中的基础元素
这个变量根据自己需要,决定是否定义为引用- 每次迭代,declaration 部分的变量会被初始化为 expression部分的下一个元素值
例如,遍历一个数组:
#include <iostream>
#include <cstdlib>
int main(void)
{
int array[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
// 传统遍历数组的方法
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)
{
std::cout << *p << " ";
}
std::cout << std::endl;
// C++11 的范围 for
for (auto e : array)
{
std::cout << e << " ";
}
std::cout << std::endl;
system("pause");
return 0;
}
这样的 for 循环,对于遍历一个有范围的集合是不是很方便呢?还不用判断边界条件!!!
2. 范围 for 的使用条件
-
for 循环迭代的范围必须是确定的
对数组而言,范围就是数组中的第一个元素到最后一个元素
对于类而言,这个类必须提供 begin 和 end 方法,begin 和 end 就是 for 循环迭代的范围! -
迭代的对象要实现 ++ 和 == 的操作
五、C++11 的指针空值 nullptr
1. C++98 中的指针空值
对指针这个东西,我们是一点都不陌生,在定义一个指针的时候,我们常常有这样的操作:
int *p = NULL;
或者
int *p = 0;
因为使用未经初始化的指针是引发运行时错误的一大原因
对于常量 NULL
有的头文件这样定义: #define NULL 0
有的头文件这样定义: #define NULL ((void *) 0)
2. C++11中的 nullptr
nullptr 是一种特殊类型的字面值,它可以被转换为任意其他类型的指针类型。
我们看这样一段代码:
#include <iostream>
#include <cstdlib>
void func(int a)
{
std::cout << "void func(int)" << std::endl;
}
void func(int *p)
{
std::cout << "void func(int *)" << std::endl;
}
int main(void)
{
func(0);
func(NULL);
func(nullptr);
system("pause");
return 0;
}
编译执行:
通过这个重载函数我们看到,对 NULL 的处理本应该是参数为指针的 func 函数,却变成参数为整型的 func 函数,而实参为 nullptr 成功调用参数为指针的 func 函数。
在新标准下,现在的 C++ 程序建议使用 nullptr ,同时尽量避免使用 NULL。
3. decltype
我们再来看一个 C++11 新标准引入的第二个类型说明符 decltype
,它的作用是选择并返回操作数的数据类型。
编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; // sum 的类型就是函数 f 的返回类型
4. nullptr_t
nullptr 是有类型的,其类型为 nullptr_t,仅仅可以被隐式转换为指针类型。
typedef decltype(nullptr) nullptr_t;