目录
一、内联函数
1.内联函数的定义与底层机制
0x01.内联函数的定义
0x02.内联函数的底层机制
2.内联函数的优缺点
优点:
缺点:
3.内联函数的使用建议
4.内联函数的注意事项
二、auto关键字(C++11)
1.代码示例
2.auto使用场景
0x01.迭代器遍历
0x02. Lambda表达式
0x03.范围for循环
0x04.复杂的类型推导
0x05.与decltype结合使用
3.使用auto关键字注意事项
0x01. 初始化是必需的
0x02.auto 不能作为函数的参数
0x03.auto 不能直接用来声明数组
0x04.避免过度使用
0x05.注意const和volatile的保留
0x06.小心引用和指针的推导
0x07.初始化表达式中的类型转换
0x08.与模板结合使用时的小心
4.auto关键字总结
三、基于范围的for循环(C++11)
1.范围for的语法
2. 范围for的使用条件
3.底层实现原理
4.细节和注意事项
5.范围for总结
四、指针空值 nullptr(C++11)
1.nullptr的引入背景
2.nullptr与NULL的区别
3.nullptr的使用场景
0x01.初始化指针
0x02.比较指针
0x03.函数参数和返回值
0x05.避免类型混淆
4.注意事项
5.总结
一、内联函数
在C++编程中,内联函数(Inline Function)是一种强大的优化手段,它通过减少函数调用的开销来提高程序的执行效率。这里将深入探讨C++内联函数的底层机制、优缺点以及最佳实践,并插入一些示例代码以帮助你更好地理解。
1.内联函数的定义与底层机制
内联函数是C++语言中的一种特性,它允许程序员请求编译器将函数的定义(实现)直接插入到所有调用该函数的地方,而不是创建一个新的函数调用栈帧。这意味着当函数被调用时,它的代码会直接嵌入到调用处,而不是跳转到函数定义处执行。
0x01.内联函数的定义
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调 用建立栈帧的开销,内联函数提升程序运行的效率。
正常函数调用
内联函数调用
从这里可以看到内联函数并没有建立栈帧,减少建立栈帧的开销从而提高程序的效率。
0x02.内联函数的底层机制
-
内联请求与编译器决策:
- 当程序员使用
inline
关键字声明一个函数时,他们实际上是在向编译器提出一个内联请求。 - 编译器会根据函数的复杂性、大小、调用频率以及编译器的优化策略来决定是否接受这个请求。
- 如果编译器决定不接受内联请求,该函数将被视为普通函数进行处理。
- 当程序员使用
-
内联函数的展开:
- 当编译器接受内联请求时,它会在每个调用该函数的地方插入该函数的代码。
- 这意味着每次函数调用都会被替换为函数体的实际代码。
- 内联函数的展开是在编译时进行的,而不是在运行时。
-
内联函数的局限性:
- 内联函数不能包含复杂的控制结构(如循环和递归),否则编译器可能会拒绝内联请求。
- 内联函数通常用于小型、频繁调用的函数,以避免代码膨胀和编译时间增加。
2.内联函数的优缺点
优点:
- 提高执行效率:内联函数避免了函数调用的开销,包括堆栈帧分配、参数传递和返回操作。
- 改善代码可读性:对于小型、简单的函数,内联函数可以使代码更易于阅读和理解。
缺点:
- 代码膨胀:内联函数会导致调用处的代码膨胀,因为每次调用都会插入函数体的代码。
- 编译时间增加:编译器需要处理更多的代码,这可能会增加编译时间。
- 调试困难:由于代码被展开到调用点,调试时可能难以跟踪到原始的函数定义。
3.内联函数的使用建议
- 选择小型函数:仅内联小型函数,因为大型函数会产生代码膨胀,影响程序的执行效率。
- 避免递归函数:内联递归函数会导致堆栈溢出或编译器拒绝内联。
- 使用
inline
关键字:虽然inline
只是一个建议,但使用它可以增加编译器内联函数的可能性。 - 剖析代码:使用剖析工具来识别哪些函数受益于内联。
- 函数定义放在头文件中:为了确保编译器在编译每个调用该函数的源文件时都能看到函数定义,通常将内联函数的定义放在头文件中。否则内联函数会认为在调用的地方展开,导致不生成地址。
4.内联函数的注意事项
inline
关键字只是建议:inline
关键字只是向编译器提出内联请求的建议,编译器可以选择是否接受这个请求。- 避免过度使用:过度使用内联函数可能会导致代码膨胀和编译时间增加,从而降低程序的性能。
- 考虑代码的可移植性:在某些情况下,内联函数可能会导致代码在不同编译器或平台上的行为不一致。因此,在编写跨平台代码时,应谨慎使用内联函数。
二、auto关键字(C++11)
在C++11标准中,auto
关键字被赋予了新的使命——类型推导。在此之前,C++中的auto
主要用于声明变量的存储期为自动,即局部变量。然而,从C++11开始,auto
关键字主要用于根据变量的初始化表达式自动推导其类型。
1.代码示例
int main()
{
auto i = 42; // i的类型将被推断为int
auto ch = 'a';// ch的类型将被推断为char
auto d = 3.14; // y的类型将被推断为double
auto z = "hello"; // z的类型将被推断为const char*
cout << typeid(i).name() << endl; // i
cout << typeid(ch).name() << endl; // c
cout << typeid(d).name() << endl; // d
cout << typeid(z).name() << endl; // P
return 0;
}
运行结果如下 :
2.auto使用场景
0x01.迭代器遍历
在使用STL容器(如std::vector
、std::list
等)时,auto
可以自动推导迭代器的类型,避免显式指定迭代器类型的繁琐。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
0x02. Lambda表达式
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> vec = {1, 2, 3, 4, 5};
auto print = [](int x) { std::cout << x << " "; };
std::for_each(vec.begin(), vec.end(), print);
std::cout << std::endl;
return 0;
}
0x03.范围for循环
范围for循环是C++11引入的一种简化数组或容器遍历的语法,auto
可以自动推导集合元素的类型。
#include <iostream>
#include <vector>
int main()
{
std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto num : vec) {
std::cout << num << std::endl;
}
return 0;
}
0x04.复杂的类型推导
在处理复杂类型(如模板实例化后的类型、类型别名等)时,auto
可以简化类型声明。
#include <iostream>
#include <map>
#include <string>
int main()
{
std::map<std::string, int> myMap = {{"apple", 1}, {"banana", 2}};
for (const auto& pair : myMap) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
// 使用auto简化类型推导,避免显式指定std::pair<const std::string, int>
auto it = myMap.find("apple");
if (it != myMap.end()) {
std::cout << "Found apple with value: " << it->second << std::endl;
}
return 0;
}
0x05.与decltype
结合使用
在某些情况下,你可能需要保留变量的const
或volatile
属性,或者需要推导出一个与现有变量相同类型的新变量。这时可以使用decltype
与auto
结合。
#include <iostream>
int main()
{
int x = 10;
decltype(auto) y = x; // y的类型是int,与x相同
const int z = 20;
decltype(auto) w = z; // w的类型是const int,保留了z的const属性
std::cout << "y: " << y << ", w: " << w << std::endl;
// 注意:不能修改w的值,因为它是const int类型
// w = 30; // 错误:不能给常量赋值
return 0;
}
3.使用auto关键字注意事项
0x01. 初始化是必需的
auto
变量必须在使用前进行初始化,因为auto
是通过初始化表达式来推导类型的。未初始化的auto
变量将导致编译错误。
auto x; // 错误:auto变量必须初始化
auto y = 10; // 正确:y的类型被推导为int
0x02.auto 不能作为函数的参数
#include<iostream>
using namespace std;
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{
}
0x03.auto 不能直接用来声明数组
#include<iostream>
using namespace std;
int main()
{
int a[] = { 1,2,3 };
auto b[] = { 4,5,6 };
return 0;
}
0x04.避免过度使用
虽然auto
可以简化代码,但过度使用可能会降低代码的可读性。特别是在类型信息对于理解代码逻辑很重要时,显式指定类型可能更清晰。
// 不推荐:过度使用auto可能导致代码难以阅读
auto a = 5;
auto b = "Hello";
auto c = std::make_pair(a, b);
// 推荐:在类型明确且重要时,显式指定类型
int a = 5;
std::string b = "Hello";
std::pair<int, std::string> c = std::make_pair(a, b);
0x05.注意const
和volatile
的保留
auto
在推导类型时不会保留顶层const
和volatile
限定符。如果你需要保留这些限定符,可以使用decltype(auto)
。
const int x = 10;
auto y = x; // y的类型是int,丢失了const限定符
decltype(auto) z = x; // z的类型是const int,保留了const限定符
0x06.小心引用和指针的推导
当使用auto
推导引用或指针类型时,要确保初始化表达式也是引用或指针,以避免意外的类型推导。
int x = 10;
int& ref = x;
auto a = ref; // a的类型是int,不是int&
auto& b = ref; // b的类型是int&,正确推导为引用
int* ptr = &x;
auto c = ptr; // c的类型是int*,不是int**
auto* d = ptr; // d的类型是int*,正确推导为指针
0x07.初始化表达式中的类型转换
如果初始化表达式涉及类型转换,auto
将推导转换后的类型。
double d = 3.14;
auto e = static_cast<int>(d); // e的类型是int,因为进行了静态类型转换
0x08.与模板结合使用时的小心
在模板编程中,auto
可以用于类型推导,但要小心模板参数的类型推导和auto
的交互。
template<typename T>
void func(T x)
{
auto y = x; // y的类型与x相同
}
int main()
{
func(42); // T被推导为int,y的类型也是int
func(3.14); // T被推导为double,y的类型也是double
}
4.auto关键字总结
auto
关键字在C++中主要用于类型推导,特别是在处理STL容器、Lambda表达式、范围for循环以及复杂类型时,auto
可以大大简化代码的编写。然而,过度使用auto
可能会降低代码的可读性,特别是在类型信息对于理解代码逻辑至关重要时。因此,在使用auto
时,需要权衡代码的简洁性和可读性。
三、基于范围的for循环(C++11)
在C++11中,范围for循环(Range-based for loop)是一项重要的新特性,它提供了一种简洁且高效的方法来遍历容器或数组中的元素。下面将深入探讨这一特性,通过代码和图片详细解释其工作原理和底层实现。
1.范围for的语法
在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;
cout << array[i] << " ";
}
cout<<endl;
for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
{
cout << *p <<" ";
}
cout << endl;
}
int main()
{
TestFor();
return 0;
}
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因 此C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范 围内用于迭代的变量,第二部分则表示被迭代的范围。
void TestFor()
{
int array[] = { 1, 2, 3, 4, 5 };
for (auto& e : array)
e *= 2;
for (auto e : array)
cout << e << " ";
}
int main()
{
TestFor();
return 0;
}
注意:与普通循环类似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
2. 范围for的使用条件
对于数组而言,就是数组中第一个元素和最后一个元素的范围;
对于类而言,应该提供 begin和end的方法,begin和end就是for循环迭代的范围。
注意:以下代码就有问题,因为for的范围不确定。
void TestFor(int array[])
{
for(auto& e : array)
cout<< e <<endl;
}
这里传递过来的是数组的首元素地址,并不是数组,它会不知道范围是多少,所以会 报错。
不仅如此,并且迭代的对象要实现 ++ 和 == 的操作。(关于迭代器这个问题,以后会讲,现在提一下,没办法讲清楚,现在uu了解一下就可以了)
3.底层实现原理
// 原始的范围for循环
for (int x : vec)
{
std::cout << x << std::endl;
}
// 编译器可能生成的等价代码
auto it = vec.begin();
auto end = vec.end();
while (it != end)
{
int x = *it;
std::cout << x << std::endl;
++it;
}
4.细节和注意事项
-
类型推导:范围for循环中的变量类型(如上面的
int x
)是通过类型推导(type deduction)来确定的。编译器会根据容器或数组中的元素类型来推导变量的类型。 -
常量性:如果希望遍历过程中不修改容器中的元素,可以将变量声明为常量引用类型,例如
const auto& x : vec
。 -
范围for循环的限制:范围for循环不能用于修改容器的大小(如添加或删除元素),因为它不提供对迭代器的直接访问。如果需要修改容器,应使用传统的基于迭代器的循环。
-
数组和容器的兼容性:范围for循环不仅适用于标准容器(如
std::vector
、std::list
等),还适用于原生数组和C风格的数组。 -
性能考虑:对于简单的容器和数组,范围for循环的性能与基于迭代器的循环相当。然而,在某些复杂情况下(如需要频繁修改迭代器或进行复杂的条件判断),手动管理迭代器可能会提供更精细的控制和可能的性能优化。
5.范围for总结
总的来说,范围for循环是C++11引入的一个非常有用的特性,它简化了容器和数组的遍历操作,并提高了代码的可读性和可维护性。尽管它在底层是通过编译器转换为基于迭代器的循环来实现的,但这一转换对用户来说是透明的,使得用户能够专注于更高层次的逻辑和算法实现。
四、指针空值 nullptr
(C++11)
在C++编程中,指针是一个非常重要的概念,它允许我们直接操作内存地址。然而,指针的使用也伴随着一定的风险,特别是当指针未初始化或指向无效内存时。为了解决这个问题,C++11引入了一个新的关键字nullptr
,以替代传统的空指针常量NULL
或整数0
。接下来将深入探讨nullptr
的底层机制、使用场景以及它如何帮助提高代码的安全性和可读性。
1.nullptr
的引入背景
在C++11之前,我们通常使用NULL
或0
来表示空指针。然而,这两种方式都存在一些问题:
NULL
通常被定义为((void*)0)
,这是一个类型转换表达式,而不是一个真正的关键字。这可能导致在某些上下文中出现类型不匹配的问题。- 使用
0
作为空指针常量虽然简单,但它与整数字面量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(0);
f(NULL);
f((int*)NULL);
return 0;
}
该程序的本意是想通过 Func(NULL) 调用指针版本的 Func(int*) 函数,
但是由于 NULL 被定义成0,这么一来就不符合程序的初衷了。
在 C++98 中,字面常量 0 既可以是一个整型数字,也可以是无类型的指针 (void*) 常量,
但是编译器默认情况下会将其看成一个整型常量,
如果要将其按照指针方式来使用,必须对其进行强制类型转换 (void*)0 。
为了解决这些问题,C++11引入了nullptr
,它是一个类型安全的空指针常量。
2.nullptr
与NULL
的区别
NULL
是一个宏定义,而不是关键字。它在C++中通常被定义为0
或((void*)0)
,这可能导致类型不匹配的问题。nullptr
是一个关键字,它的类型是nullptr_t
,是一个专门用于表示空指针的类型,与传统的空指针常量NULL
(通常被定义为((void*)0)
或0
)不同,nullptr
只能被隐式转换为指针类型,而不能被转换为整数或其他非指针类型。这种类型安全性有助于减少类型混淆和潜在的错误。具有类型安全性。
3.nullptr
的使用场景
0x01.初始化指针
使用nullptr
来初始化指针是一个好习惯,它可以帮助我们避免未初始化指针导致的未定义行为。
int* ptr = nullptr;
0x02.比较指针
使用nullptr
来比较指针是否为空,可以提高代码的可读性和安全性。
if (ptr == nullptr)
{
// ptr is null
}
0x03.函数参数和返回值
在函数参数和返回值中使用nullptr
可以明确表示一个指针参数或返回值是可选的或可以为空。
void* allocateMemory(size_t size)
{
if (size == 0) {
return nullptr;
}
// Allocate memory and return pointer
}
0x05.避免类型混淆
使用nullptr
可以避免将整数0
误用作指针,从而减少类型混淆和潜在的错误。
int* ptr = nullptr; // Clear and safe
// int* ptr = 0; // Less clear and potentially error-prone
4.注意事项
- 避免类型混淆:不要将
nullptr
与整数0
混用,以避免类型混淆和潜在的错误。 - 初始化指针:始终在声明指针时初始化它,即使你暂时不知道它将指向什么。使用
nullptr
是一个好选择。 - 检查空指针:在删除指针或访问指针所指向的内存之前,始终检查指针是否为空。
- 避免悬挂指针:在删除动态分配的内存后,将指针设置为
nullptr
以避免悬挂指针的问题。 - 模板中的使用:当将
nullptr
应用于模板时,模板会将其作为一个普通的类型来进行推导,并不会将其视为T*
指针。因此,在模板函数中处理空指针时需要注意这一点。
5.总结
nullptr
是C++11引入的一个非常重要的特性,它提高了指针操作的安全性和可读性。通过使用nullptr
,我们可以避免类型混淆、减少潜在的错误,并编写更清晰、更健壮的代码。因此,在C++11及更高版本中,我们应该优先使用nullptr
来替代传统的空指针常量NULL
或整数0
。
本篇博客到此结束,如有错误之处,望各位指正~