C++的8个基础语法(下)

简介: C++的8个基础语法

内联函数

定义

以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

我们知道,在创建函数调用时,会产生一个函数栈帧,占用内存,如果我们函数中里面还有很多个不同或者是相同的函数(比如一个自定义函数当中有很多个交换两个数据的自定义函数),那么会栈溢出:

假设函数1,2,3,4都是一个函数,那么我们在C语言当中可以把这个函数定义成宏,在预编译的时候,头文件会被展开,宏会进行文本替换,所以,就不会创建这么多的函数栈帧。

不过用宏代替函数,写的时候非常麻烦,稍微不注意就会运算错误,并且还不能进行调试。

在C++中,用inline关键字修饰函数,会改掉宏替代函数的缺点,在调用的地方直接展开。

代码1:

#include <iostream>
using namespace std;
int add(int x, int y)
{
  return x + y;
}
int main()
{
  int x = 10;
  int y = 20;
  int z = add(x, y);
  cout << z << endl;
}

没有inline修饰的函数在汇编时会有call add(调用add函数)

代码2

#include <iostream>
using namespace std;
inline int add(int x, int y)
{
  return x + y;
}
int main()
{
  int x = 10;
  int y = 20;
  int z = add(x, y);
  cout << z << endl;
}

再查看这段代码时要做一些设置(因为debug下面需要调试,所以默认不会展开):

这里就没有去调用add函数,说明展开了。

特性

当然,也不是什么函数都能修饰成内联函数的。

不然,去把递归的函数修饰一下,岂不是很精彩。

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
#include <iostream>
using namespace std;
inline int add(int x, int y)
{
  int sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  sum = x + y;
  return sum;
}
int main()
{
  int x = 10;
  int y = 20;
  int z = add(x, y);
  cout << z << endl;
}

所以,内联函数最好还是被调用频繁,而且行数不多的函数。

3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。

我们知道,在一个项目中,往往函数的定义和声明不在同一个文件内,所以编译器链接的时候,会有一个符号表,里面会存入函数的地址,然后再调用函数的时候会在符号表中找这个函数的地址从而进行调用。

然而用inline修饰过的函数却不会出现在函数表内,这时编译也不会通过:

这是编译报错,意思是链接错误。

因为每个文件都是自己工作自己的,不会干扰到其他文件,当编译器看到函数被inline修饰的时候就已经不会把地址放在符号表里面了,至于编译器忽略iniline的事情是再调用的时候,所以不用担心。

这个程序当中,头文件虽然在源文件展开,但是并没有函数的定义在源.cpp展开,所以没办法被使用。

当我们把定义和声明放在一起就可以运行了。

auto关键字

类型别名的思考

在C++当中会有一些很长的类型名,例:

#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给类型取别名,那么我们就要记住很多别名,非常麻烦。

auto定义

auto作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

typeid(b).name()的意思是打印b的类型

#include <iostream>
using namespace std;
int main()
{
  int a = 10;
  auto b = a;//这里自动推导a赋值给b的类型是什么
  auto c = "a";
  auto& d = a;
  auto* e = &a;
  cout << typeid(b).name() << endl;
  cout << typeid(c).name() << endl;
  cout << typeid(d).name() << endl;
  cout << typeid(e).name() << endl;
  return 0;
}

注意

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

使用细则

用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。

#include <iostream>
using namespace std;
int main()
{
  int a = 10;
  auto b = &a;
  auto& d = a;
  auto* e = &a;
  cout << typeid(b).name() << endl;
  cout << typeid(d).name() << endl;
  cout << typeid(e).name() << endl;
  return 0;
}

当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。

不能推导的场景

auto不能作为函数的参数

因为函数在创建栈帧的时候编译器无法推导a类型,也就无法得知栈帧创建的具体大小。

auto不能直接用来声明数组

基于范围的for循环

定义

for循环后的括号由冒号“ :”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围。

这里等于把arr数组遍历一遍然后赋值给e。

for里面的arr数组是会遍历整个数组的。

当然如果你想改里面的值就要用引用了,指针是不行的,因为数组里面的都是int类型的。

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

范围for的使用条件

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

对于数组而言,就是数组中第一个元素和最后一个元素的范围。

对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。

int test(int arr[])//因为传过来的不是数组,而是指针,所以无法确定数组的范围
{
  for (auto& e : arr)
    cout << e << endl;
}

迭代的对象要实现++和==的操作。(这个以后说)

指针空值nullptr

我们平时初始化一个指针如果不知道他指向谁,那么就要让他指向一个空指针(NULL),这是一个良好的习惯。

其实NULL是一个宏,C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0//也就是说NULL等于0
#else
#define NULL   ((void *)0)
#endif
#endif

C++也是一样的,那么使用重载函数的时候就会遇到以下问题:

如果想让NULL走第二个函数就要强制转换类型,因为库不能随意改动,只能打补丁,所以就出现了一个关键字nullptr,效果是和NULL一样的,只不过没有这个BUG。

注意:

  1. 在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
  2. 在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。
  3. 为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
相关文章
|
1月前
|
编译器 C++ 容器
C++语言的基本语法
想掌握一门编程语言,第一步就是需要熟悉基本的环境,然后就是最重要的语法知识。 C++ 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。现在让我们简要地看一下什么是类、对象,方法、即时变量。 对象 - 对象具有状态和行为。例如:一只狗的状态 - 颜色、名称、品种,行为 - 摇动、叫唤、吃。对象是类的实例。 类 - 类可以定义为描述对象行为/状态的模板/蓝图。 方法 - 从基本上说,一个方法表示一种行为。一个类可以包含多个方法。可以在方法中写入逻辑、操作数据以及执行所有的动作。 即时变量 - 每个对象都有其独特的即时变量。对象的状态是由这些即时变量的值创建的。 完整关键字
40 2
|
2月前
|
Java 编译器 程序员
C++中的语法知识虚继承和虚基类
**C++中的多继承可能导致命名冲突和数据冗余,尤其在菱形继承中。为解决这一问题,C++引入了虚继承(virtual inheritance),确保派生类只保留虚基类的一份实例,消除二义性。虚继承通过`virtual`关键字指定,允许明确访问特定路径上的成员,如`B::m_a`或`C::m_a`。这样,即使基类在继承链中多次出现,也只有一份成员副本,简化了内存布局并避免冲突。虚继承应在需要时提前在继承声明中指定,影响到从虚基类派生的所有后代类。**
51 7
|
2月前
|
编译器 C++ 开发者
C++一分钟之-属性(attributes)与属性语法
【7月更文挑战第3天】C++的属性(attributes)自C++11起允许附加编译器指令,如`[[nodiscard]]`和`[[maybe_unused]]`,影响优化和警告。注意属性放置、兼容性和适度使用,以确保代码清晰和可移植。示例展示了如何使用属性来提示编译器处理返回值和未使用变量,以及利用编译器扩展进行自动清理。属性是提升代码质量的工具,但应谨慎使用。
62 13
|
3月前
|
编译器 程序员 C++
C++一分钟之-属性(attributed)与属性语法
【6月更文挑战第28天】C++的属性为代码添加元数据,帮助编译器理解意图。C++11引入属性语法`[[attribute]]`,但支持取决于编译器。常见属性如`nodiscard`提示检查返回值,`maybe_unused`防止未使用警告。问题包括兼容性、过度依赖和误用。使用属性时需谨慎,确保团队共识,适时更新以适应C++新特性。通过示例展示了`nodiscard`和`likely/unlikely`的用法,强调正确使用属性能提升代码质量和性能。
57 13
|
3月前
|
编译器 C语言 C++
|
3月前
|
存储 自然语言处理 编译器
【C++语言1】基本语法
【C++语言1】基本语法
|
4月前
|
编译器 C语言 C++
C++的基本特性和语法
C++的基本特性和语法
52 1
|
4月前
|
存储 C++ 容器
【C++从练气到飞升】09---string语法指南(二)
【C++从练气到飞升】09---string语法指南(二)
|
4月前
|
存储 Linux C语言
【C++从练气到飞升】09---string语法指南(一)
【C++从练气到飞升】09---string语法指南(一)