C++入门(lesson2)(下)

简介: C++入门(lesson2)

2.3特性🐱


1、inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提升程序运行效率。


2、inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的标准,取决于编译器内部实现)、不是递归、且频繁调用的小函数采用inline修饰,否则编译器会忽略inline特性。下图为《C++prime》第五版关于inline的建议:


1669254336999.jpg


3、inline不建议声明和定义分离,分离会导致链接错误。因为inline在预处理阶段被展开,没有有效函数地址了,链接会找不到。


4、如果函数调用过多,比如有10000个调用的地方,这时候就不建议使用内联函数了。因为比如说内联函数有30行代码,inline展开就有30W行代码,而这些代码是冗余的,无用重复代码,他会大大增加可执行程序的大小。这时就使用普通函数即可,它只需要消耗10000+30行代码。


对于第三点,我们可以在编译器上来查看一下:


// 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 __cdecl 
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用


这里为什么会报链接错误呢?我们在学习C语言预处理时知道,程序执行要经过预处理-->编译-->汇编-->链接这几个阶段,而链接要完成的任务是:


1、合并段表,相同的段进行合并;


2、符号表的合并和重定义,选取有效地址。


这里报错是因为符号表合并找不到有效地址而出错。为什么找不到呢?


因为在C++编译器中,当编译器遇到inline关键字后,除了会替换之外,内联函数就不会进入符号表,(这一点和宏是契合的,预处理之后不进入符号表)所以当替换的是内联函数的声明,而不是定义且在当前文件下找不到定义的时候,就会出现错误。


所以这里的建议是,编写内联函数在头文件的时候,不要声明,直接定义在头文件。


如果内联函数是解决宏函数缺陷的问题,那么对于宏定义的比如常量等其他问题我们是如何解决的呢?


C++通常用这些技术替代宏:


1、常量定义换用const enum


2、短小函数定义 换用内联函数。


三、auto关键字🐅


3.1类型别名思考🐱


为什么引入auto这个关键字呢?因为随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:


1、类型难于拼写


2、含义不明确导致容易出错


比如:


#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是一个类型,但是该类型太长了,特别容易写错。有读者可能就会提出:可以通过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; }

但是还是不够方便。在编程时,常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而有时候要做到这点并不容易,因此C++11给auto赋予了新的含义。


3.2auto简介🐱


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;
  return 0;
}

1669254402218.jpg


这里的typeid().name()的作用是拿到变量的类型。


注意:


使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种"类型"的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。


3.3 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; 
}

②.在同一行定义多个变量😀

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译

器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量 。

1669254454380.jpg


3.4 auto不能推导的场景🐱


①auto不能作为函数的参数😀

void Func(auto x)
{
  printf("Func(int x)\n");
}
void Func(int x)
{
  printf("Func()\n");
}
int main()
{
  Func(5.2);
  Func(5);
  return 0;
}
//错误  C2668 “Func”: 对重载函数的调用不明确

这里,很显然,如果使用auto,编译器是无法区分参数类型的,因为auto任何类型都可以兼容,这样和函数重载也会冲突。


②auto不能直接用来声明数组😀

void TestAuto()
{
  int a[] = { 1,2,3 };
  auto b[] = { 4,5,6 };
}
//错误  C2119 "b": 无法从空的初始值设定项推导 "auto []" 的类型  
//错误  C3318 “auto []”: 数组不能具有其中包含“auto”的元素类型

为了避免与 C++98 中的 auto 发生混淆, C++11 只保留了 auto 作为类型指示符的用法。

auto 在实际中最常见的优势用法就是跟以后会讲到的 C++11 提供的新式 for 循环,还有

lambda 表达式等进行配合使用。


四、基于范围的for循环🐅


4.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;
for (int* p = array; p < array + sizeof(array)/ sizeof(array[0]); ++p)
     cout << *p << endl; 
}

对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时还会容易犯错,因此C++11中引入了基于范围的for循环。for循环后的括号由冒号":"分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。


比如我们想打印一个数组,这样就可以:


void TestFor()
{
  int array[] = { 1, 2, 3, 4, 5 };
  for (auto e : array)
  cout << e << " ";
  return;
}

但是,如果我们想对数组里的元素操作,这样是否可以呢?


1669254496188.jpg


我们发现,数组里的数据并没有乘以2,说明我们遍历的数组,不是数组本身,而是一个临时拷贝,并不影响数组的值,所以我们如果想对数组里的数操作,需要使用引用:


1669254504150.jpg


注意:范围for与普通循环类似,是可以用continue来结束本次循环,也可以用break来跳出整个循环。


4.2范围for的使用条件🐱


①.for循环迭代的范围必须是确定的😀


对于数组而言,就是数组中第一个元素和最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。


注意:对于传参的数组,是不能用范围for循环的。


void TestFor(int a[])
{
  for (auto& e : a)
  cout << e << endl;
}
int main()
{
  int a[] = { 1,2,4,5,6,7 };
  TestFor(a);
  return 0;
}

像这样,传过去的只是一个指针,也就是数组首个元素的地址。


五、指针空值nullptr(C++11)🐅


5.1 C++98中的指针空(NULL)🐱


我想先问一个问题,NULL是一个地址吗?如果你觉得NULL是一个地址,那么请看以下代码:


1669254538186.jpg


如果NULL是地址,那么为什么会调用第二个函数而打印出f(int)呢?


这其实和C++98的bug有关,C98中对于NULL的定义,我们可以在头文件(stddef.h)中看到

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

我们看到,NULL实际是个宏,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量,上面很显然程序将它作为0来处理而不是空值的指针。


5.2 空值指针nullptr🐱


为了应对这种错误,C++11引入了nullptr,将其定义为(void*)0,它是一个关键字,不需要引头文件。


注意:


1、在C++11中,sizeof(nullptr)sizeof((void*)0)所占的字节数相同。


2、为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。

相关文章
|
1月前
|
安全 编译器 程序员
【C++初阶】C++简单入门
【C++初阶】C++简单入门
|
17天前
|
编译器 Linux C语言
C++基础入门
C++基础入门
|
1月前
|
安全 编译器 C++
C++入门 | 函数重载、引用、内联函数
C++入门 | 函数重载、引用、内联函数
25 5
|
1月前
|
存储 安全 编译器
C++入门 | auto关键字、范围for、指针空值nullptr
C++入门 | auto关键字、范围for、指针空值nullptr
49 4
|
1月前
|
编译器 C语言 C++
C++入门 | 命名空间、输入输出、缺省参数
C++入门 | 命名空间、输入输出、缺省参数
33 4
|
1月前
|
编译器 程序员 C语言
C++入门
C++入门
31 5
|
1月前
|
安全 编译器 C语言
C++入门-数组
C++入门-数组
|
1月前
|
存储 编译器 程序员
C++从遗忘到入门
本文主要面向的是曾经学过、了解过C++的同学,旨在帮助这些同学唤醒C++的记忆,提升下自身的技术储备。如果之前完全没接触过C++,也可以整体了解下这门语言。
|
2月前
|
存储 编译器 C++
C++从遗忘到入门问题之float、double 和 long double 之间的主要区别是什么
C++从遗忘到入门问题之float、double 和 long double 之间的主要区别是什么
|
2月前
|
编译器 C++
C++从遗忘到入门问题之C++中的浮点数类型问题如何解决
C++从遗忘到入门问题之C++中的浮点数类型问题如何解决