目录
1.内联函数
1.1概念
在C语言中有宏,宏在调用的地方会展开,但是宏有很多的缺点:不便于调试,没有类型检查,可读性差等,所以c++中引入了内联函数,关键字inline修饰函数,程序中的内联函数在调用的时候会在调用的地方展开,这样就没有了函数调用建立栈桢的开销 ,内联函数提升了程序的运行效率,例如:
#include<iostream>
inline void Swap(int& num1, int& num2)
{
int tmp = num1;
num1 = num2;
num2 = tmp;
}
int main()
{
int a = 28;
int b = 30;
Swap(a, b);
std::cout << a << " " << b;
return 0;
}
上述函数会在调用的地方展开,编译期间编译器会用函数体来替换函数的调用方式。
查看方式:
设置好这些我们打开调试转到反汇编 就会看见内联函数的展开。
1.2特性
1.内联函数是一种以空间换时间的做法, 内联函数会在编译的时候在内联函数调用的地方替换成函数体。优点是节省函数调用的开销,提高了程序的效率,缺点是可能使目标文件过大。
2.inline只是给编译器的建议,是否作为内联函数由编译器决定,一般函数体小(函数规模小),不是递归且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性,下图为《C++prime》第五版关于inline的建议:
3.inline 不建议声明和定义分开。分离会产生链接错误。因为编译的时候inline被展开,所以没有函数地址。链接就找不到了。
例如:
//test.c #include<iostream> #include<Swap.h> int main() { int a = 28; int b = 30; Swap(a, b); std::cout << a << " " << b; return 0; }
//Swap.h inline void Swap(int& num1, int& num2);
//Swap.c inline void Swap(int& num1, int& num2) { int tmp = num1; num1 = num2; num2 = tmp; }
宏的优缺点:
优点:代码复用性好,提高性能。
缺点:没有类型检查。不便于调试,可读性差,可维护性差,容易出错。
2.auto关键字
2.1类型别名思考
随着程序越来越复杂,程序中用到的类型也越来越复杂了,体现在:
#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;
}
std::map<std::string, std::string>::iterator 是一个类型,但是这个类型太长了,大家可能想到给它起个别名不就好了,例如:
#include <string>
#include <map>
typedef std::map<std::string, std::string>::iterator mapiter
int main()
{
std::map<std::string, std::string> m{ { "apple", "苹果" }, { "orange",
"橙子" },
{"pear","梨"} };
mapiter it = m.begin();
while (it != m.end())
{
//....
}
return 0;
}
typedef起别名确实可以简化程序,但是typedef会遇到新的难题我们看看下面的代码:
typedef char* pstring;
int main()
{
const pstring p1; // 编译成功还是失败?
const pstring* p2; // 编译成功还是失败?
return 0;
}
上面的代码会产生下面的问题,所以使用typedef起别名的方式也会存在问题 。
在编程时常常将表达式的值赋给变量。这就要求定义变量的时候要清楚的知道表达式的类型,有时候这并不是件容易的事情,因此c++11标准赋予了auto新的含义。
2.2auto简介
早期C语言中auto关键字是用来修饰局部变量的,因为局部变量进入作用域,自动创建,出作用域自动销毁,但是因为局部变量之前的auto是可以省略的所以一般都不写。
所以c++11中,标准委员会 赋予auto全新的含义:即auto不在是一个存储类型指示符,而是作为一个新的类型指示器符来指示编译器,auto声明的变量必须由编译器在编译期间推导而得。
2.3auto的使用规则
auto关键字定义的变量可以自动识别类型,例如:
#include<iostream>
int main()
{
int a = 0;
auto b = a;
int c = 0;
auto d = &c;
auto* f = &c;//推测指针类型
auto& g = a;
std::cout << typeid(b).name() <<std::endl;
std::cout << typeid(a).name() << std::endl;
std::cout << typeid(c).name() << std::endl;
std::cout << typeid(d).name() << std::endl;
std::cout << typeid(f).name() << std::endl;
std::cout << typeid(g).name() << std::endl;
return 0;
}
1.通过上面的代码我们可以发现auto可以推测各种类型。
auto在声明指针类型时auto和auto*没有任何区别。但是auto声明引用时必须加&:auto&。
注意使用auto关键字必须初始化变量。 在编译期间编译器会根据初始化表达式的值来推导auto的实际类型。因此auto并非一种类型的声明,而是类型声明时的占位符,编译器在编译期间会将auto替换成为实际的变量类型。
2.在同一行声明多个变量时,这些变量必须是相同的类型不然编译器会报错,因为编译器实际上只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
#include<iostream>
int main()
{
auto a = 10, b = 5.5;
return 0;
}
上面的代码会报错。
2.4auto不能推导的场景
1.auto不能做函数的参数
例如:
#include<iostream>
int Add(auto num1, auto num2)//auto做函数的参数会报错
{
return num1 + num2;
}
int main()
{
int a = 10;
int b = 20;
int sum = Add(a, b);
return 0;
}
2.auto不能推测数组
#include<iostream>
int main()
{
auto arr[10] = { 0 };
return 0;
}
注意:为了避免冲突c++11中只保留了auto作为类型指示符的用法,
auto最大的优势是用于基于范围的for循环等。
3.基于范围的for循环
3.1范围for的语法
以前我们使用for循环都是怎么使用的呢?如下:
#include<stdio.h>
int main()
{
int arr[10] = { 0 };
for (int i = 0; i < 10; i++)
{
arr[i] = i;
}
for (int i = 0; i < 10; i++)
{
arr[i] *= 2;
printf("%d ", arr[i]);
}
}
现在c++11标准提供了基于范围的for循环,这种用法很简洁。如下:
#include<iostream>
int main()
{
int arr[10] = {0 };
for (auto &e : arr)
e = 1;
for (auto e : arr)
std::cout << e << std::endl;
return 0;
}
3.2范围for的使用局限
for循环迭代的范围必须确定对于数组而言就是数组的第一个元素和最后一个元素。对于类而言,应该提供begin和end的方法,因为begin和end就是循环迭代的范围。
在函数中是没办法使用的,因为数组传参传递给函数的实际上是指针,(数组传参会退化为指针 ),如下:(这个代码是错误的,因为for的范围无法确定)
#include<iostream>
void Print(int arr[10])
{
for (auto e : arr)
{
std::cout << e << std::endl;//这样是使用不了的
}
}
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
Print(arr);
return 0;
}
基于范围的for循环有:分为两部分,第一部分是范围内用于迭代的变量,第二部分是则表示被迭代的范围。它和普通循环一样也可以使用break结束循环,使用continue结束本次循环。
4.空指针nullptr
4.1c98中的空指针值
在良好的编程习惯中,声明一个变量时最好给这个变量一个初始值,否则,可能会出现各种问题,比如未初始化的指针,如果直接解引用就会出现问题,如果一个指针没有合法化的指向,我们一般都会让它指向空(NULL)。如下:
int main()
{
int* ptr = NULL;//初始化ptr
return 0;
}
NULL实际上是一个宏,在传统的c文件stddef.h中,可以看到如下的代码:
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
可以看到NULL可能被定义为字面常量0,或者被定义为无类型的指针(void*)的常量,不论采取哪种定义,在使用空指针时,都不可避免遇到一些麻烦,比如:
#include<stdio.h>
void Fi(int)
{
printf("这是整形\n");
}
void Fi(int*)
{
printf("这是整形指针\n");
}
int main()
{
Fi(0);
Fi(NULL);
Fi((int*)NULL);
return 0;
}
这个代码输出的结果就和我们预期的结果不一样,程序的本意是想通过Fi(NULL)来调用指针版本的函数Fi(int*),但是由于NULL被定义为0,因此与程序的初衷相悖。
在c++98中,字面常量既可以是一个整形数字,也可以是无类型的指针常量(void*),但是编译器默认情况将其看做一个整形常量,如果要将其按照指针的形式来使用就要对其进行强转(void*)0。
注意:在使用nullptr表示空指针时,不需要包含头文件,因为nullptr是c++11引入的关键字。
c++11中sizeof(nullptr)与sizeof((void*)0)的字节数相同。
为了提高代码的健壮性建议使用nullptr来代替NULL。