【C++】C++入门

在这里插入图片描述

关于C++是什么

C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机,20世纪80年代,计算机界提出了OOP(object oriented programming:面向对象)思想,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了面向对象的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计
C++在许多领域都有广泛的应用,包括游戏开发、系统/应用软件、驱动和嵌入式系统开发、高性能服务器和客户端应用,以及创建大型复杂的图形用户界面和数据库应用。

一、关键字

C语言中的关键字有32个,C++中的关键字有63个,几乎增长了一倍。

asmdoifreturntrycontinueautodoubleinline
shorttypedefforbooldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrowcaseenummutable
staticunionwchar_tcatchnamespaceexplicitstatic_castunsigneddefault
charexportnewstructusingfriendclassexternoperator
switchvirtualregisterconstfalseprivatetemplatevoidtrue
whileprotectedthisvolatileconst_castdeletetemplategotoreinterpret_cast

二、命名空间

注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中

2.1 命名空间的作用

命名空间能够避免命名冲突。当模块越来越大时,不同模块中可能存在相同名称的类、函数等。使用命名空间可以将相关的类和函数分组,有效避免这些名称冲突。

例子:在下面这个代码中就会发生命名冲突,原因是因为全局中定义了一个rand变量,而头文件#include <stdlib.h> 中包含了rand函数的声明,这样就导致了命名冲突,最终编译报错。

#include <stdio.h>
#include <stdlib.h>//命名冲突
int rand = 0;int main()
{printf("%d", rand);return 0;
}

在这里插入图片描述

2.2 命名空间定义

定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。

下面代码就展示了如何定义命名空间,关键字 + 命名空间的名字 ,aj 是我CSDN的名字,这里就用他来做我命名空间的名字,而命名空间的名字是可以按照自己的想法自由定义。

namespace aj  //我CSDN的名字
{int rand = 0;
}

命名空间的成员不仅仅可以是内置类型、自定义类型,还可以是函数。

namespace aj  //我CSDN的名字
{//内置类型int rand = 0;//函数void Swap(int* e1, int* e2){int tmp = *e1;*e1 = *e2;*e2 = tmp;}//自定义类型struct SeqList{int* a;int size;int capacity;};
}

命名空间能够嵌套使用

namespace aj  //我CSDN的名字
{//内置类型int rand = 0;//函数void Swap(int* e1, int* e2){int tmp = *e1;*e1 = *e2;*e2 = tmp;}//自定义类型struct SeqList{int* a;int size;int capacity;};namespace aj1{int rand = 1;int Add(int x1, int x2){return x1 + x2;}}}

同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。

namespace aj  //我CSDN的名字
{int i = 0;
}namespace aj  //我CSDN的名字
{double d = 0; 
}/*namespace aj  //我CSDN的名字
{int i = 0;double d = 0;
}*/

在这里可以试验出两个相同名称的命名空间编译并不会报错,前面两个命名空间会合并成第三个命名空间。
在这里插入图片描述


2.3 命名空间的使用

定义一个命名空间就相当于产生了一个新的作用域,在全局中并不能直接使用命名空间中的成员。

使用命名空间中成员的方法一共有三种:

  1. 加命名空间名称及作用域限定符 ( :: 作用域限定符)
  2. 用 using 将命名空间内某个成员引入 ( using + 命名空间名称 :: 成员名)
  3. 使用 using namespace 加命名空间名称引入

  • 加命名空间名称及作用域限定符 ( :: 作用域限定符)
namespace aj  //我CSDN的名字
{//内置类型int rand = 0;
}int main()
{printf("%d\n", aj::rand);  //命名空间中的randprintf("%p\n", rand);      // #inlcude <stdlib.h> 中 rand 函数地址return 0;
}
  • 用 using 将命名空间内某个成员引入 (using + 命名空间名称 :: 成员名)

在下面的代码中,我使用 (using + 命名空间名称 :: 成员名 )的方法将命名空间中的成员i 引入,那么成员 i 就能够在全局中被使用,而 d 未被引入,使用 d 就会报错。

namespace aj  //我CSDN的名字
{//内置类型int i = 0;double d = 0.0;
}using aj::i;int main()
{printf("%d", i);printf("%lf", d);return 0;
}

在这里插入图片描述
注意:如果使用 using + 命名空间名称 :: 成员名导入的成员与函数名相同,会产生名称冲突。

在这里插入图片描述

  • 使用 using namespace 加命名空间名称引入
namespace aj  //我CSDN的名字
{//内置类型int i = 0;double d = 0.0;
}using namespace aj;int main()
{printf("%d", i);printf("%lf", d);return 0;
}

在这里插入图片描述
using namespace 加命名空间名称普通using作用相同,但前者是对整个命名空间作用,把该命名空间下的所有类、函数、常量等均导入当前作用域。
注意:若将整个命名空间中的所有成员全部导入当前作用域,那么就相当于这个命名空间失效,则有没有这个命名空间都相同,所以说这种方法不是很推荐,推荐前两种方法。


三、C++ 的输入、输出

  • C++中使用cout 标准输出对象cin 标准输入对象时,必须包含#include <iostream >头文件以及按命名空间使用方法使用std

    cout --> console 控制台 + out cout 表示标准输出,它输出到控制台。
    cin --> console 控制台 + in cin 表示标准输入,它从键盘接收输入。

#include<iostream>
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std ;int main()
{cout << "Hello C++" << endl; return 0;
}
  • endl ( end of line ) 是特殊的C++符号,表示换行输出 ,与'\n' 的作用相同。endlcin、cout 都需要包 #include <iostream> 才能使用。
    在这里插入图片描述

  • <<是流插入运算符,>>是流提取运算符。

在这里插入图片描述

  • 相比于C语言中的输入输出,C++输入输出更方便,C++的输入输出可以自动识别变量类型,C语言中的输入输出需要自己判断输出变量的类型。
#include<iostream>
using namespace std ;int main()
{int num1 = 0;int num2 = 0;//C语言的输入输出scanf("%d", &num1);printf("%d\n", num1);//C++的输入输出cin >> num2;cout << num2 << endl;return 0;
}

在这里插入图片描述

注意:std 是C++标准库的命名空间
在我们日常敲代码的时候,代码量不会很大,只需要 using namespace std 展开命名空间。
而当我们做项目的时候代码量很大,这样做会将C++的标准库全部展开,会很容易产生命名冲突 , 所以建议前面提到的第一种 (std::cin)指定命名空间成员和第二种方法(using std::cin)展开常用函数 / 类型


四、缺省函数

4.1 缺省参数概念

缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。

#include<iostream>
using namespace std;void Print(int x = 0)
{cout << x << endl;
}int main()
{Print();Print(520);Print(1314);return 0;
}

在这里插入图片描述

4.2 缺省函数分类

4.2.1. 全缺省函数

全缺省函数是在定义函数的时候所有参数都有默认值

#include<iostream>
using namespace std;void Print(int x = 1, int y = 2, int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;
}int main()
{Print();Print(10);Print(10, 20);Print(10 , 20 ,30);return 0;
}

在这里插入图片描述

4.2.2. 半缺省函数(部分缺省函数)

半缺省函数是在定义函数的时候部分参数有默认值

#include<iostream>
using namespace std;void Print(int x , int y = 2, int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;cout << endl;}int main()
{Print(10);Print(10, 20);Print(10, 20, 30);return 0;
}

注意

  1. 半缺省函数的指定参数必须从右向左给
//错误示范
void Print(int x = 1 , int y , int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;cout << endl;
}

在这里插入图片描述

  1. 调用缺省函数时,传参必须从左向右传,不能指定传参。
void Print(int x = 1, int y = 2, int z = 3)
{cout << x << endl;cout << y << endl;cout << z << endl;cout << endl;}int main()
{Print(, 20 ,);Print(, , 30);return 0;
}

例如我只想给第二个位置传一个值,或是只给第三个位置传一个值,这样传参是不被允许的。
在这里插入图片描述

  1. 缺省参数不能在函数声明和定义中同时出现
// test1.h
void Func(int a = 10);
// test1.cpp
void Func(int a = 20){}
//注意:如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值。

在这里插入图片描述

4.3 使用缺省值的好处

namespace aj
{typedef struct Stack{int* _a;int _top;int _capacity;}ST;void StackInit(ST* st){st->_a = NULL;st->_capacity = 0;st->_top = -1;}
}

在初始化栈的时候,有的人会先开一部分空间,有的人会选择先不给空间,但是都不可避免的是,如果入栈个数很多的情况下会进行大量的扩容行为,扩容也是有消耗的,所以不提倡大量的扩容行为。那么有的人会说,初始化的时候开一个大空间,那么就会有下面三个问题

  • 首先开多少空间叫做大空间
  • 其次是入栈次数的情况下会浪费空间
  • 最后是我们定义多个栈且知道需要的栈容量不同的时候,每个栈的初始空间都相同显然是不合理的。

下面这个代码就能知道缺省值能够起多么大的作用。

namespace aj
{typedef struct Stack{int* _a;int _top;int _capacity;}ST;// 缺省值为4,默认开四个int大小空间// 若显示传参,那么则开参数个int大小的空间void StackInit(ST* st, int N = 4){st->_a = (int*)malloc(sizeof(int) * N);st->_capacity = N;st->_top = 0;}void StackPush(ST* st, int x){// ...// 入栈操作在栈中有详细讲解,这里省略}
};int main()
{// 需要入栈5个数据aj::ST st1;aj::StackInit(&st1, 5);for (int i = 0; i < 5; i++){aj::StackPush(&st1, i);}// 需要入栈500个数据aj::ST st2;aj::StackInit(&st2, 500);for (int i = 0; i < 500; i++){aj::StackPush(&st2, i);}// 不知道需要入栈多少个数据,默认申请四个空间aj::ST st3;aj::StackInit(&st3);return 0;
}

五、函数重载

5.1 函数重载的概念

函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数或类型或类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。

  • 同名函数的形参列表(参数个数或类型或类型顺序)不同
// 参数类型不同
void func(int x, int y)
{// ...
}void func(double x, double y)
{// ...
}// 参数顺序不同
void func(char x, int y)
{// ...
}void func(int x, char y)
{// ...
}// 参数个数不同
void func(char x, char y)
{// ...
}void func(char x, char y, char z)
{// ...
}
  • C++允许在同一作用域中声明几个功能类似的同名函数

下面的两个函数是否是函数重载呢?显然不是,这两个函数根本不在同一个作用域中。

namespace aj1
{void func(int x, int y){// ...}
}namespace aj2
{void func(double x, double y){// ...}
}

在这里插入图片描述

在不同的作用域中,就算函数一模一样也不构成函数重载。

namespace aj1
{void func(int x, int y){// ...}
}namespace aj2
{void func(int x, int y){// ...}
}

在这里插入图片描述
那么下面两个函数构成函数重载吗?构成,原因是命名空间的名字相同,内容会合并,两个函数的参数不同,即构成函数重载。

namespace aj1
{void func(int x, int y){// ...}
}namespace aj1
{void func(double x, double y){// ...}
}

函数重载这个语法能够带来很多便捷。例如Swap函数 、 Add函数等等。
实现同种功能,但参数类型不同的时候非常好用,使用的时候像是使用同一个函数,但实际上不是。

// C语言的写法
void Swapi(int* e1, int* e2)
{int tmp = *e1;*e1 = *e2;*e2 = tmp;
}void Swapd(double* e1, double* e2)
{double tmp = *e1;*e1 = *e2;*e2 = tmp;
}// C++写法
void Swap(int* e1, int* e2)
{int tmp = *e1;*e1 = *e2;*e2 = tmp;
}void Swap(double* e1, double* e2)
{double tmp = *e1;*e1 = *e2;*e2 = tmp;
}

5.2 C++支持函数重载的原理–名字修饰(name Mangling)

为什么C++支持函数重载,而C语言不支持函数重载
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
在这里插入图片描述

Test.cpp
预处理     头文件展开、宏替换、删除注释、条件编译Test.cpp 通过 预处理 生成  Test.i
编译       检查语法、生成汇编指令Test.i 通过 编译 生成  Test.s 
汇编       将汇编代码转换为二进制机器码Test.s 通过 汇编 生成  Test.o
链接       合并链接,生成可执行程序Test.o 通过 链接 生成	a.out / Test.exe

下面用VS来测试一下编译后C语言、C++的函数会被修饰成什么。
C++中:

void func(int x, double y);
//{
//	cout << "func(int x, double y)" << endl;
//}void func(double x, int y);
//{
//	cout << "func(double x, int y)" << endl;
//}int main()
{func(2, 2.2);func(2.2, 2);return 0;
}

在这里插入图片描述
解释一下为什么这里会报错:
由于函数的地址实际上是函数第一句指令的地址,那么函数只有声明,那么这个函数就不会进入符号表中,当调用这个函数的时候通过修饰后的名字在符号表中寻找函数的地址,找不到,就报错。

C语言中:

void func(int x, double y);
//{
//	//cout << "func(int x, double y)" << endl;
//}int main()
{func(2, 2.2);return 0;
}

在这里插入图片描述
很明显C++中同名函数的参数不同,那么修饰出来的名字也不同,而C语言中并没有函数名修饰,这里的函数调用只是在函数前面加了一个_ , 这样就能知道为什么C语言中不允许出现同名函数(也就是函数重载),而C++中可以支持。


由于VS下的函数名修饰规则过于复杂,接下来使用Linux看一下gcc编译器下的函数名修饰规则。
在这里插入图片描述

void func(double x, int y)
{cout << "func(double x, int y)" << endl;
}int main()
{func(2.2, 2);return 0;
}

在这里插入图片描述

void func(int x, double y)
{cout << "func(int x, double y)" << endl;
}void func(double x, int y)
{cout << "func(double x, int y)" << endl;
}int main()
{func(2, 2.2);func(2.2, 2);return 0;
}

通过上面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】。
并且上面的图片也能证明函数的地址是函数第一条指令的地址。


g++的函数名修饰规则是【_Z+函数长度+函数名+类型首字母】,里面并不没有与返回值相关的内容,所以返回值不同不能构成函数重载。
如果说允许定义的时候,函数参数相同,返回值不同,那么能构成函数重载吗?不行,看下面两个函数,调用函数时并不知道会调用哪个,存在调用歧义。所以说记住返回值不同不能构成函数重载。

void func(int x, double y)
{cout << "func(int x, double y)" << endl;
}int func(int x, double y)
{cout << "func(int x, double y)" << endl;
}int main()
{func(2, 2.2);return 0;
}

六、 引用

6.1 引用的概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

比如:周树人,他的笔名是鲁迅,但是周树人和鲁迅是同一个人

在这里插入图片描述

类型& 引用变量名(对象名) = 引用实体;

int main()
{int a = 0;int& ra = a;cout << &a << endl;cout << &ra << endl;return 0;
}

在这里插入图片描述
注意:** 引用类型必须和引用实体同种类型**的


6.2 引用特性

  1. 引用在定义时必须初始化
int main()
{int& a;return 0;
}

在这里插入图片描述

  1. 一个变量可以有多个引用
int main()
{int a = 0;int& b = a;int& c = a;return 0;
}
  1. 引用一旦引用一个实体,再不能引用其他实体
int main()
{int a = 0;int& ra = a;int m = 1;ra = m;return 0;
}

在这里插入图片描述

6.3 常引用

注意:

  1. 值返回、类型提升、类型截断都会先将值赋给临时变量,而临时变量具有常属性(不能被改变)。
  2. 权限可以缩小、可以平移不可以放大
int main()
{const int a = 0;// int& b = a; // 错误,a有const限制,所以b必须有const限制,这里是权限放大// int b = a,  // 正确,赋值不受限制const int x = 0;const int& y = x;      // 正确,这里是权限的平移int m = 0;const int& n = m;      // 正确,这里是权限的缩小int i = 0;double d = i;         // 正确,赋值不受限制// double& ri = i;    // 错误,类型提升,生成临时变量,具有常属性const double& ri = i; return 0;
}

6.4 使用场景

  1. 做参数
void Swap(int& e1, int& e2)
{int tmp = e1;e1 = e2;e2 = tmp;
}

以前写Swap函数的时候需要将两个变量的地址传过去,再解引用,现在引用实现了形参的改变影响实参

在C语言中,有部分题目会出现参数做返回值的,也叫输出型参数,以前需要传地址,现在只需要传引用。

int BtreeSize(struct TreeNode* root)
{return root == NULL ? 0 : BtreeSize(root->left) + BtreeSize(root->right) + 1;
}void _preorderTraversal(struct TreeNode* root, int* arr , int* pi)
{if(root == NULL)return;arr[(*pi)++] = root->val;_preorderTraversal(root->left, arr , pi);_preorderTraversal(root->right, arr , pi);
}int* preorderTraversal(struct TreeNode* root, int* returnSize){*returnSize = BtreeSize(root);    //遍历二叉树,记录节点个数int* arr = (int*)malloc(*returnSize*sizeof(int));int i = 0;_preorderTraversal(root, arr , &i);//用子函数递归遍历字符串return arr;
}

例如上面这个代码中的 *returnSize 就是输出型参数,可以使用引用。在子函数中我们希望 i 都是同一个 i , 所以传地址,但是使用引用改造一下会更加符合我们的使用习惯。

int BtreeSize(struct TreeNode* root)
{return root == NULL ? 0 : BtreeSize(root->left) + BtreeSize(root->right) + 1;
}void _preorderTraversal(struct TreeNode* root, int* arr , int& ri)
{if(root == NULL)return;arr[ri++] = root->val;_preorderTraversal(root->left, arr , ri);_preorderTraversal(root->right, arr , ri);
}int* preorderTraversal(struct TreeNode* root, int& returnSize){returnSize = BtreeSize(root);    //遍历二叉树,记录节点个数int* arr = (int*)malloc(*returnSize*sizeof(int));int i = 0;_preorderTraversal(root, arr , i);//用子函数递归遍历字符串return arr;
}

  1. 做返回值
int& count()
{static int n = 0; n++;return n;
}

注意: 如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用引用返回,如果已经还给系统了,则必须使用传值返回。


6.5 传值、传引用效率比较

以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

接下来使用在网上找来的两段代码,来分别测试传值和传引用的效率。

  1. 测试的当参数分别是是传值和传引用的效率
#include <time.h>
struct A { int a[10000]; };
void TestFunc1(A a) {}
void TestFunc2(A& a) {}
void TestRefAndValue()
{A a;// 以值作为函数参数size_t begin1 = clock();for (size_t i = 0; i < 10000; ++i)TestFunc1(a);size_t end1 = clock();// 以引用作为函数参数size_t begin2 = clock();for (size_t i = 0; i < 10000; ++i)TestFunc2(a);size_t end2 = clock();// 分别计算两个函数运行结束后的时间cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
}

在这里插入图片描述

  1. 测试的当返回值分别是是传值和传引用的效率
#include <time.h>
struct A{ int a[10000]; };
A a;
// 值返回
A TestFunc1() { return a;}
// 引用返回
A& TestFunc2(){ return a;}
void TestReturnByRefOrValue()
{
// 以值作为函数的返回值类型
size_t begin1 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc1();
size_t end1 = clock();
// 以引用作为函数的返回值类型
size_t begin2 = clock();
for (size_t i = 0; i < 100000; ++i)
TestFunc2();
size_t end2 = clock();
// 计算两个函数运算完成之后的时间
cout << "TestFunc1 time:" << end1 - begin1 << endl;
cout << "TestFunc2 time:" << end2 - begin2 << endl;
}

在这里插入图片描述
通过上面两段代码可以发现,传值和传引用的效率明显是传引用的效率更高


6.6 指针和引用的区别

语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

底层实现上实际是有空间的,因为引用是按照指针方式来实现的

int main()
{int a = 0;int* pa = &a;int& ra = a;return 0;
}

在这里插入图片描述

int main()
{int a = 0;int* pa = &a;int& ra = a;(*pa)++;ra++;return 0;
}

在这里插入图片描述

引用指针的不同点:

  1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
  2. 引用在定义时必须初始化,指针没有要求
  3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
  4. 没有NULL引用,但有NULL指针
  5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
  6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
  7. 有多级指针,但是没有多级引用
  8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
  9. 引用比指针使用起来相对更安全

七、 内联函数

7.1 内联函数的概念

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

inline void Swap(int& e1, int& e2)
{int tmp = e1;e1 = e2;e2 = tmp;
}int main()
{int a = 0, b = 1;Swap(a , b);return 0;
}

在这里插入图片描述
在这里插入图片描述
从上面两张图片中我们能看到,内联函数调用的时候并不是call跳转到函数上,而是替换。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。

7.2 内联函数的特性

  1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
  2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
    内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求。
  3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
// test1.h
#include <iostream>
using namespace std;
inline void func(int x);// test1.cpp
#include "test1.h"
void func(int x)
{cout << x << endl;
}// test.cpp
#include "test1.h"int main()
{func(520);return 0;
}

在这里插入图片描述
原因: 当函数为内联函数时调用函数时不会直接跳转到函数内,而是将调用语句替换成函数,那么在编译过程中认为没有必要形成指令,在汇编过程中,函数名也不会进入符号表,在 test.cpp文件中展开 test1.h 文件,而 test.h文件中只有声明,在调用过程中没有内容可以替换,那么就会去调用函数,而在符号表中又找不到这个函数,那么就会报错。

7.3 设计内联函数的目的

设计内联函数的目的是用来替换宏函数。

  • 宏函数的缺点
    (1)容易出错,语法坑很多
    (2)不能调试
    (3)没有类型的安全检查
  • 宏函数的优点
    (1) 没有严格的类型检查
    (2)当频繁调用一个小函数时,将函数写成宏函数,调用函数时不会建立栈帧,提高效率

下面写几种常见 Add宏函数 来说明为什么宏函数容易出错。

//#define Add(a , b) a+b          // (1)
//#define Add(a , b) a+b;         // 错误 , 宏定义时后面不能加分号
//#define Add(a , b) (a)+(b)	  // (2)
//#define Add(a , b) ((a)+(b))    // (3)int Add(int a, int b)             // (4)  
{return a + b;
}int main()
{int a = 3;int b = 5;int mul = Add(a|b, a&b) * 7;// (1)  =>  a | b + a & b * 7  => 3 | 5 + 3 & 5 * 7// 3 | 5 + 3 & 8  => 3 | 8 & 35  => 1 & 35 => 1// (2)  =>  (a) | (b) + (a) & (b) * 7  => (3 | 5) + (3 & 5) * 7// 1 + 1 * 7  => 1 + 7  => 8// (3)  =>  ((a) | (b) + (a) & (b)) * 7  => ((3 | 5) + (3 & 5)) * 7// (1 + 1) * 7  => 2 * 7  => 14// (4)  =>  2 * 7 => 14return 0;
}

上述宏函数定义完全正确的只有(3),调用宏函数时,无论参数是数字还是表达式都是直接替换,操作符的优先级会影响表达式的结果,而当我们写函数的时候,基本上不会出现这类问题,因为函数传参时会将表达式计算后传过去。

设计出内联函数在调用的地方直接展开可以直接替换掉宏函数,而后面学习的模版能够解决类型的问题。
除了宏函数,还有宏常量,可以用 constenum替换宏常量。


八、auto关键字(C++11)

8.1类型别名思考

随着程序越来越复杂,程序中用到的类型也越来越复杂,经常体现在:

  1. 类型难于拼写
  2. 含义不明确导致容易出错

auto 能够通过右边的值,自动推算出左边的类型。

#include <iostream>
using namespace std;#include <vector>
#include <string>int main()
{int a = 0;auto b = a;auto c = &a;auto& d = a;// 普通场景没有什么价值// 类型很长,就有价值,简化代码std::vector<std::string> v;std::vector<std::string>::iterator it = v.begin();//auto it = v.begin();cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;cout << typeid(it).name() << endl;return 0;
}

在这里插入图片描述
std::map<std::string, std::string>::iterator 是一个类型,但是该类型太长了,特别容易写错。

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

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

8.2 auto的使用细则

  1. auto与指针和引用结合起来使用
    用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&
int main()
{int a = 0;auto b = &a;auto* c = &a;auto& d = a;cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;return 0;
}

在这里插入图片描述

  1. 在同一行定义多个变量
    当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
int main()
{auto m = 520, n = 521;//auto x = 520, y = 521.0;// 这里编译会出错,因为x,y的初始化类型不同return 0;
}

在这里插入图片描述

8.3 auto不能推导的场景

  1. auto 不能作为函数的参数
//此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void func(auto i)
{//
}

在这里插入图片描述

  1. auto 不能直接用来声明数组
int main()
{auto arr[] = { 1 , 2 , 3 };return 0;
}

在这里插入图片描述

九、 基于范围的for循环(C++11)

9.1 范围for的语法

在以前没有范围 for 的时候,下面的代码就是我们常见遍历数组的方式

void func()
{int arr[] = { 1, 2, 3, 4, 5 };for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){arr[i] *= 2;}for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); p++){cout << *p << endl;}
}

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

void func()
{int arr[] = { 1, 2, 3, 4, 5 };for (auto& e : arr){e *= 2;}for (auto e : arr){cout << e << " ";}
}

9.2 范围for的使用条件

for循环迭代的范围必须是确定的
对于数组而言,就是数组中第一个元素和最后一个元素的范围 ;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
注意: 以下代码就有问题,因为for的范围不确定

void func(int arr[])
{for (auto& e : arr){cout << e << endl;}
}

原因是范围 for 使用的参数必须是数组名,如果将数组名作为函数参数传递给函数,那么在函数内部这个参数被看作是一个指针,而不是数组,所以说这个代码有问题。

十. 指针空值nullptr(C++11)

nullptr的出现实际上是为了补救 C++ 98 中 NULL的坑。
NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

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

对于学习C语言的人来说, NULL 就是被当做空指针使用,而 C++ 中却把 NULL 定义成 0 ,与我们学习的C语言相悖,但是因为有些程序员就是把NULL当做0来使用,也不能将NULL的内容直接改变,所以nullptr就出现了。

注意:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的。
2.在C++11中,sizeof(nullptr)sizeof((void*)0)所占的字节数相同。
3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。


结尾

本次的内容就到此为止了,对于今天的内容,对于一个萌新来说会有一点难理解,但是一定要对这一部分非常熟悉,这部分内容会在后面的知识中反复的被使用。
C++的出现是因为 本贾尼·斯特劳斯特卢普(Bjarne Stroustrup 原来以C语言为主要编程语言,但他觉得C语言在一些面向对象的特性和容易出错的问题上还不完善,需要改进。这就动机他基于C语言研发出C++,弥补C语言的短板,成为当时更适合软件开发的语言。


如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
这一篇万字C++入门也是写了一段时间,如果这篇文章对你有用的话,请大家给一个三连支持一下!!🌹🌹
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/259341.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

静态时序分析:SDC约束命令set_clock_latency详解

相关阅读 静态时序分析https://blog.csdn.net/weixin_45791458/category_12567571.html?spm1001.2014.3001.5482 时钟的延迟可以使用set_clock_latency命令设置&#xff0c;这里的时钟延迟包括源延迟(source latency)&#xff0c;即时钟对象到时钟源对象&#xff08;时钟定义…

Linux——网络通信TCP通信常用的接口和tcp服务demo

文章目录 TCP通信所需要的套接字socket()bind()listen()acceptconnect() 封装TCP socket TCP通信所需要的套接字 socket() socket()函数主要作用是返回一个描述符&#xff0c;他的作用就是打开一个网络通讯端口&#xff0c;返回的这个描述符其实就可以理解为一个文件描述符&a…

抖音关键词搜索爬虫,抖音API数据接口,抖音商品详情数据采集

抖音商品API接口抖音关键词搜索抖音直播间小黄车抖店商品数据采集 除了微博&#xff0c;小红书&#xff0c;抖音也是一个巨大的流量池。 除了评论&#xff0c;其实关键词搜索视频是更为常见的一个需求&#xff0c;于是上周末抽空开发了下&#xff0c;完成了 mvp。

数据结构——lesson3单链表介绍及实现

目录 1.什么是链表&#xff1f; 2.链表的分类 &#xff08;1&#xff09;无头单向非循环链表&#xff1a; &#xff08;2&#xff09;带头双向循环链表&#xff1a; 3.单链表的实现 &#xff08;1&#xff09;单链表的定义 &#xff08;2&#xff09;动态创建节点 &#…

【数据结构】链表OJ面试题5《链表的深度拷贝》(题库+解析)

1.前言 前五题在这http://t.csdnimg.cn/UeggB 后三题在这http://t.csdnimg.cn/gbohQ 给定一个链表&#xff0c;判断链表中是否有环。http://t.csdnimg.cn/Rcdyc 给定一个链表&#xff0c;返回链表开始入环的第一个结点。 如果链表无环&#xff0c;则返回 NULLhttp://t.cs…

OpenCV识别人脸案例实战

使用级联函数 基本流程 函数介绍 在OpenCV中&#xff0c;人脸检测使用的是cv2.CascadeClassifier.detectMultiScale()函数&#xff0c;它可以检测出图片中所有的人脸。该函数由分类器对象调用&#xff0c;其语法格式为&#xff1a; objects cv2.CascadeClassifier.detectMul…

vue-进阶语法(四)

目录 v-model原理 v-model应用于组件 sync修饰符 ref 和 $refs&#xff08;重点&#xff09; $nextTick v-model原理 原理&#xff1a;v-model本质上是一个语法糖。例如应用在输入框上&#xff0c;就是 value属性 和 input事件 的合写。 作用&#xff1a;提供数据的双向…

[NSSRound#16 Basic]Web

1.RCE但是没有完全RCE 显示md5强比较&#xff0c;然后md5_3随便传 md5_1M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2&md5_2M%C9h%FF%0E%E3%5C%20%95r%D4w…

ClickHouse--08--SQL DDL 操作

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 SQL DDL 操作1 创建库2 查看数据库3 删除库4 创建表5 查看表6 查看表的定义7 查看表的字段8 删除表9 修改表9.1 添加列9.2 删除列9.3 清空列9.4 给列修改注释9.5 修…

大数据01-导论

零、文章目录 大数据01-导论 1、数据与数据分析 **数据&#xff1a;是事实或观察的结果&#xff0c;是对客观事物的逻辑归纳&#xff0c;是用于表示客观事物的未经加工的原始素材。**数据可以是连续的值&#xff0c;比如声音、图像&#xff0c;称为模拟数据&#xff1b;也可…

【网络安全】什么样的人适合学?该怎么学?

有很多想要转行网络安全或者选择网络安全专业的人在进行决定之前一定会有的问题&#xff1a; 什么样的人适合学习网络安全&#xff1f;我适不适合学习网络安全&#xff1f; 当然&#xff0c;产生这样的疑惑并不奇怪&#xff0c;毕竟网络安全这个专业在2017年才调整为国家一级…

Web 扫描神器:WhatWeb 保姆级教程(附链接)

一、介绍 WhatWeb 是一款用于识别网站技术栈和特征的开源Web扫描工具。它可以自动分析网站的响应并识别出使用的Web框架、CMS、服务器、JavaScript库等技术组件。WhatWeb的目标是通过分析网站的内容&#xff0c;提供有关目标的技术信息&#xff0c;这对于安全测试、漏洞评估和…

Jetpack Compose 第 2 课:布局

点击查看&#xff1a;Jetpack Compose 教程 点击查看&#xff1a;Composetutorial 代码 简介 Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API&#xff0c;可以帮助您简化并加快 Android 界面开发。 在本教程中&a…

Quartz---基础

1.概述 Quartz是一个完全由Java编写的开源任务调度框架&#xff0c;通过触发器来设置作业定时运行规则&#xff0c;控制作业的运行时间。Quartz框架的主要核心组件包括调度器、触发器和作业。调度器作为作业的总指挥&#xff0c;触发器作为作业的操作者&#xff0c;而作业则为应…

前端常见的设计模式

说到设计模式&#xff0c;大家想到的就是六大原则&#xff0c;23种模式。这么多模式&#xff0c;并非都要记住&#xff0c;但作为前端开发&#xff0c;对于前端出现率高的设计模式还是有必要了解并掌握的&#xff0c;浅浅掌握9种模式后&#xff0c;整理了这份文章。 六大原则&…

【图像分割 2023 WACV】HiFormer

【图像分割 2023 WACV】HiFormer 论文题目&#xff1a;HiFormer: Hierarchical Multi-scale Representations Using Transformers for Medical Image Segmentation 中文题目&#xff1a;HiFormer:基于Transformer的分层多尺度表示医学图像分割 论文链接&#xff1a; 论文代码&a…

代码随想录算法训练营第三十四天|860.柠檬水找零 406.根据身高重建队列 452. 用最少数量的箭引爆气球

860.柠檬水找零 链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 细节&#xff1a; 1. 首先根据题意就是只有5.的成本&#xff0c;然后就开始找钱&#xff0c;找钱也是10.和5. 2. 直接根据10 和 5 进行变量定义&#xff0c;然后去循环…

Vue3+Vite+TS+Pinia+ElementPlus+Router+Axios创建项目

目录 初始项目组成1. 创建项目1.1 下载项目依赖1.2 项目自动启动1.3 src 别名设置vite.config.ts配置文件tsconfig.json配置若新创项目ts提示 1.4 运行测试 2. 清除默认样式2.1 样式清除代码下载2.2 src下创建公共样式文件夹style2.3 main.js中引入样式2.4 安装sass解析插件 2.…

数据分析(一) 理解数据

1. 描述性统计&#xff08;summary&#xff09; 对于一个新数据集&#xff0c;首先通过观察来熟悉它&#xff0c;可以打印数据相关信息来大致观察数据的常规特点&#xff0c;比如数据规模&#xff08;行数列数&#xff09;、数据类型、类别数量&#xff08;变量数目、取值范围…

剪辑视频衔接怎么操作 剪辑视频衔接过渡自然方法 剪辑视频教程新手入门 抖音剪辑短视频 会声会影视频制作教程

视频剪辑在现代社交媒体和数字媒体时代中变得越来越重要。它广泛应用于各种领域&#xff0c;包括电影制作、广告宣传、教育培训、社交媒体内容创作等。 一、剪辑视频衔接怎么操作 会声会影是一款功能强大、易于使用的视频编辑软件。接下来我们拿会声会影为例讲解剪辑视频如何…