C++入门·收尾
|
文章目录
- 学习网站
- 写在前面
- 内联函数
- 概念
- 特性
- 面试题
- auto关键字(C++11)
- auto简介
- auto的使用细则
- auto不能推导的场景
- 基于范围的for循环(C++11)
- 范围for的语法
- 范围for的使用条件
- 指针空值nullptr(C++11)
- C++98中的指针空值
- 大厂面试真题
学习网站
推荐给老铁们两款学习网站:
面试利器&算法学习:牛客网
风趣幽默的学人工智能:人工智能学习
首个付费精品专栏:《C++入门核心技术》
写在前面
前面讲到的函数重载和引用是C++入门部分的重点,校招时也很重要,所以大家要牢牢掌握哦,今天的内容就简单多了,加油吧少年们。
内联函数
概念
以 inline 修饰的函数叫做内联函数,编译时 C++编译器会在调用内联函数的地方直接展开,没有函数压栈的开销,内联函数提升程序运行的效率。
int Add(int left, int right)
{
return left + right;
}
int main()
{
int ret = 0;
ret = Add(1, 2);
return 0;
}
上面的Add函数是普通函数,调用时会建立栈帧,压栈等一系列操作,如果频繁调用Add函数,效率上会有一定的损失。
那C语言是如何解决频繁调用像Add这种短小函数导致效率损失的问题呢?在讲解函数重载那节时,编译的预处理过程会发生头文件展开、宏替换、删除注释、条件编译这四个过程,所以这里的答案是定义宏函数,比如:
#define ADD(x, y) ((x) + (y))
C语言是定义宏函数,那C++是如何做的呢?C++是在Add函数前增加 inline 关键字,将其改为内联函数,这样的话在编译期间编译器会用函数体替换函数的调用(在调用内联函数处函数体直接展开)。
可能老铁会有一个疑惑,为什么C语言都有了宏函数这样的方法,C++还要大费周折去搞一个inline呢?那是因为啊,宏函数极容易写错,比如少加一个括号等,所以C++引入inline解决宏函数晦涩难懂、容易写错的问题,而且宏函数还不支持调试。
特性
- inline是一种以空间换时间的做法,省去了调用函数建立栈帧的开销,所以代码很长(一般函数体在10行以内可以,具体取决于编译器)或者有循环/递归的函数都不适宜作为内联函数;
- 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(1);
return 0;
}
面试题
1、宏的优缺点?
优点:
- 增强代码的复用性;
- 提高性能。
缺点:
- 不方便调试宏;(预编译阶段进行了宏替换)
- 导致代码可读性差,可维护性差,容易误用;
- 没有类型安全的检查。
2、C++有哪些技术替代宏?
- 常量定义换用const;
- 函数定义换用内联函数。
auto关键字(C++11)
编译器支持C++11时才可以使用auto关键字。
auto简介
C++11规定 auto 作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。
int test()
{
return 10;
}
int main()
{
int a = 10;
auto b = a;
auto c = 'a';
auto d = test();
cout << typeid(a).name() << endl;//可认为typeid(x).name()返回的是变量x的类型
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的使用细则
1、auto与指针和引用结合起来使用
auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时必须加&;
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;
2、在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则会报错,因为编译器实际上只对第一个变量进行推导,然后用推导出来的的类型定义其他变量。
auto a = 1, b = 2;
auto c = 3, d = 4.0;//该行代码会编译失败,因为c和d的初始化表达式类型不同
可能有老铁会疑惑,这样看来auto好像也没有太大的用处,其实不然,不信你看:
//以后我们学习STL时可能会写这样的代码:
std::map<std::string, std::string> dict;
dict["sort"] = "排序";
dict["string"] = "字符串";
std::map<std::string, std::string>::iterator it = dict.begin();
//用auto直接就简短了很多
auto it = dict.begin();
可能老铁现在还看不懂上面的代码,没关系,你现在可以知道我要表达的意思就行了,后面这些都会详细说明。
所以auto一个很重要的意义之一就是:
类型很长时,可以不写,用auto推导。
auto不能推导的场景
1、auto不能作为函数的参数
//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导(给缺省参数也不行)
//因为不知道实参类型具体是什么,建立栈帧时不知道要建立多大
void Test1(auto a)
{}
2、auto不能直接用来声明数组
void Test2()
{
int a[] = {1, 2, 3};
auto b[] = {4, 5, 6};
}
3、为了避免与C++98中的auto发生混淆,C++11只保留了auto作为类型指示符的用法;
4、auto在实际中最常见的优势用法就是跟以后会讲到的C++11提供的新式for循环,还有lambda表达式等进行配合使用。
基于范围的for循环(C++11)
范围for的语法
在C++98中如果要遍历一个数组,可以按照以下方式进行:
int array[] = { 1, 2, 3, 4, 5, 6 };
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循环后的括号由冒号" : " 分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。
int array[] = { 1,2,3,4,5 };
//依次自动取array中的数据,赋值给e,自动判断结束
for (auto& e : array)//想想为什么用的是引用?
e *= 2;
for (auto e : array)
cout << e << " ";
如果想恢复成原来数组的数据,这样可以吗?
for(auto e : array)
e \= 2;
这样是错误的,因为e改变不会改变array数组中的值,e只是array数组值的一份拷贝,那怎么办呢?用引用即可(不是拷贝,而是别名)
注意:与普通循环相似,可以用continue来结束本次循环,也可以用break来跳出整个循环。
范围for的使用条件
1、for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是循环迭代的范围;
注意:以下代码就有问题,因为for的范围不确定。
//这里的array是指针,并不确定其范围,形参传过来的是数组首元素的地址
void TestFor(int array[])
{
for(auto& e : array)
cout << e << endl;
}
2、迭代的对象要实现++和==的操作。(关于迭代器,后面会详细讲解)
关于范围for的底层以后会讲解,大家现在了解会用即可。
指针空值nullptr(C++11)
C++98中的指针空值
在良好的C/C++编程习惯中,声明一个变量时最好给该变量一个合适的初始值,否则可能会出现不可预料的错误,比如未初始化的指针。如果一个指针没有合法的指向,我们基本都是按照如下方式对其进行初始化:
void TestPtr()
{
int* p1 = NULL;
int* p2 = 0;
}
NULL实际上是一个宏,在传统的C语言头文件(stddef.h)中,可以看到如下代码:
可以看到C++将NULL定义成了字面常量0,在实际编程中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;
}
程序本意是想通过 f(NULL) 调用指针版本 f(int*) 函数,但是由于NULL被定义成0,因此和程序的初衷相悖。
在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。
注意:
- 在使用 nullptr 表示指针空值时,不需要包含头文件,因为nullptr 是C++11作为新关键字引入的;
- 在C++11中,sizeof(nullptr) 和 sizeof((void*)0) 所占的字节数相同;
- 为了提高代码的健壮性,在后续表示指针空值时建议使用nullptr。
大厂面试真题
1、关于c++的inline关键字,以下说法正确的是( )
A.使用inline关键字的函数会被编译器在调用处展开
B.头文件中可以包含inline函数的声明
C.可以在同一个项目的不同源文件内定义函数名相同但实现不同的inline函数
D.递归函数也都可以成为inline函数
解析:文章理解之后这道题很简单,选C
2、在( )情况下适宜采用 inline 定义内联函数
A.函数体含有循环语句
B.函数体含有递归语句
C.函数代码少、频繁调用
D.函数代码多,不常调用
解析:跟上一题一样简单