C++ 【 第一回 】入门 -> 入土

C++ 【 第一回 】入门 -> 入土,第1张

1. C++关键字(C++98)

C++总计63个关键字,C语言32个关键字

以下是C++98里面的关键字

asmdoifreturntrycontinue
autodoubleinlineshorttypedeffor
booldynamic_castintsignedtypeidpublic
breakelselongsizeoftypenamethrow
caseenummutablestaticunionwchar_t
catchexplicitnamespacestatic_castunsigneddefault
charexportnewstructusingfriend
classexternoperatorswitchvirtualregister
constfalseprivatetemplatevoidtrue
const_castfloatprotectedthisvolatilewhile
deletegotoreinterpret_cast
 2.命名空间 2.1作用域规则

在C语言中我们知道他的程序中名字的作用域分为两种是 局部作用域 和 全局作用域。


1.局部变量:如果一个变量被定义在函数中,那么这个变量的作用域就是一个局部作用域,在函数体外面不能访问这个变量,在别的函数定义同名变量,编译器会给他分配新的内存,他们互补干扰,你在你的地盘,我在我的地盘。


2.静态局部变量:想要延长他的生命周期我们可以在前面加一个static关键字把他变成静态局部变量,但是他的作用跟局部变量一样,即只能在定义该变量的函数内使用该变量,只是程序仅分配一次内存,函数返回后,该变量不会消失,该变量还存在只是不能使用他。


但是他的生命周期延长到了整个工程。


 3.全局变量:在函数体外定义的变量,且存放在静态存储区中,在定义变量的位置到本源文件结束后都有效,在这个作用域中全局变量,可以为程序内各个函数引用,因此如果在一个函数中改变了全局变量的值, 就能影响到其他函数中全局变量的值。


4.静态全局变量:和全局变量一样,不过加了static之后会限制他们的作用域,使他们只能在定义他们的文件内使用,加了static可以将名字限制在一个文件中,防止名字污染。


滥用全局变量可能还是造成会造成名字污染,我加static也不行。


我们在之前说了C语言从你的文本文件变成可执行文件过程中,会经过预处理,编译,汇编,链接的过程。


而在预处理里面会有 头文件的展开,宏替换  ,取消注释  ,条件编译。


而qsort是C里面的一个快排函数,我预处理头文件展开了,恰巧有个函数声明,这个函数就叫qsort,即使你加了static关键字修饰,但是还是在本文件内使用,还是会有重定义的问题。


C语言这时只能去改这个变量的名称了,万一工程很大呢?这时为了解决命名冲突,C++里面就有了命名空间。


2.2 命名空间定义

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



1.普通的命名空间

namespace A {
	int a = 10;
	// 命名空间中的内容,既可以定义变量,也可以定义函数
	int add(int a, int b) {
		return a + b;
	}
}

2. 命名空间可以嵌套

namespace B {
	int b = 20;
	int add(int a, int b) {
		return a + b;
	}
	namespace C {
		int c = 30;
		int add(int a, int b) {
			return a + b;
		}
	}
}

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

namespace A {
	int aa = 10;
	int sub(int a, int b) {
		return a - b;
	}
}
namespace A {
	int a = 10;
	int add(int a, int b) {
		return a + b;
	}
}

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


2.3 命名空间使用

举个例子这个函数打印的是那个a呢?

#include
namespace A {
	int a = 10;
	// 命名空间中的内容,既可以定义变量,也可以定义函数
	int add(int a, int b) {
		return a + b;
	}
}

int main() {
	int a = 1000;
	printf("%d", a);
	return 0;
}

肯定是1000啦,那我怎么访问A这个命名空间里的a呢? 

命名空间的使用有三种方式:

2.3.1 加作用域限定符 ::
#include
namespace A {
	int a = 10;
	// 命名空间中的内容,既可以定义变量,也可以定义函数
	int add(int a, int b) {
		return a + b;
	}
}

int main() {
	int a = 1000;
	printf("%d", A::a);
	return 0;
}

 2.3.2 使用using关键字将命名空间引入
#include
namespace A {
	int a = 10;
	// 命名空间中的内容,既可以定义变量,也可以定义函数
	int add(int a, int b) {
		return a + b;
	}
}
using A::add;
int main() {
	int a = 10;
	int b = 20;
	printf("%d", add(a, b));
	return 0;
}

 2.3.3 使用using namespace 命名空间名称引入
#include
namespace A {
	int c = 10;
	// 命名空间中的内容,既可以定义变量,也可以定义函数
	int add(int a, int b) {
		return a + b;
	}
}
using namespace A;
int main() {
	int a = 10;
	int b = 20;
	printf("%d\n",c);
	printf("%d\n", add(a, b));
	return 0;

 3.C++输入和输出

不知道你是否和我一样在刚刚开始接触C语言的时候写过以下的代码

 这个问题确实现在看起来让人啼笑皆非,但是刚刚开始学习的时候确实让我很费解,看半天也不知道错误在哪里。


但是在C++里面输入和输出是不需要格式控制符的,输入也不需要取地址符了。


在C++中使用标准输入输出必须包含 头文件 和 std 命名空间

#include 
int main() {
	int a = 0;
	std::cin >> a;
	std::cout << a << std::endl;
}

不过我们一般不这样用会用using关键字来使用这个命名空间。


#include 
using namespace std;
int main() {
	//int a = 0;
	//std::cin >> a;
	//std::cout << a << std::endl;

	int a, b, c;
	double d;
	//可以连着输入
	cin >> a >> b >> c;
	//可以连着输入不同类型的
	cin >> a >> b >> c >> d;

	cout << a << endl;
	//还可以连着输出不同类型的
	cout << "hello world!" << 100 << 3.14 << endl;
}
4.缺省参数 4.1C和C++函数的区别

举个例子,这个代码会报错吗?

 

 答案是不会的,但是C++的编译器比C语言编译器更严格,就会报错。


 

 在看这个例子

 这个在C语言的的编译器下也是可以通过的但是在C++下就不行,C++编译器对函数的返回值类型检测的更加的严格。


 

还记得在之前写栈的实现的时候我们在初始化时候默认给了栈3个空间,在C语言的时候我们只能在调用这个方法的时候穿一个3作为参数进去,但是在C++里面我们可以这样做。


 4.1 缺省参数概念
#include 
using namespace std;
typedef struct Stack
{
	int* array;
	int size;
	int capacity;
}Stack;
void InitStack(Stack* ps, int initCapacity = 3)
{
	cout << initCapacity << endl;
}

int main()
{
	Stack s;
	InitStack(&s, 100);
	InitStack(&s);
	return 0;
}

 这个语法在C++是可以通过的,这个3相当与一个备胎你没有传参数,那我就用你了。


4.2 缺省参数概念

缺省参数是声明或定义函数时为函数的参数指定一个默认值。


在调用该函数时,如果没有指定实参则采用该默认值,否则使用指定的实参

#include 
using namespace std;
void Test(int a = 0)
{
	cout << a << endl;
}
int main()
{
	Test(); // 没有传参时,使用参数的默认值
	Test(10); // 传参时,使用指定的实参
}
4.3 缺省参数分类 4.3.1全缺省参数

所有参数都有默认参数

#include 
using namespace std;
void Test(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}
int main()
{
	Test(); // 没有传参时,使用参数的默认值
	Test(10); // 传参时,使用指定的实参
}

那我们在看这段代码

#include 
using namespace std;
void Test(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << " b = " << b << " c = " << c << endl;
}
int main()
{
	Test(); // 没有传参时,使用参数的默认值
	Test(100); // 传参时,使用指定的实参
	Test(100, 200);
	Test(100, 200, 300);
}

这里Test(100);和Test(100, 200, 300);我们都知道第一个替换和全部替换,那为啥Test(100, 200);

结果是100 200 30 不是 100 200 10呢?

 这里先卖个关子不知道大家知道 __cdecl (下面会说)

4.3.2半缺省参数

从右往左依次给出默认值

#include 
using namespace std;
void Test(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl;
}
int main()
{
	Test(10);
}

注意

1. 半缺省参数必须从右往左依次来给出,不能间隔着给(下面会说)

2. 缺省参数不能在函数声明和定义中同时出现

 如果声明与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那
个缺省值,所以声明和定义不能一起出现。


3. 缺省值必须是常量或者全局变量

4.3.4 函数调用约定

给定以下代码

#include 
using namespace std;
int Test(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << " b = " << b << " c = " << c << endl;
	return a + b + c;
}
int main()
{
	int ret =Test(100, 200, 300);
	Test(100, 200, 300);

	return 0;
}

__cdecl 这个其实是函数的调用约定,在msvc中C和C++函数调用约定都是__cdecl。


__cdecl调用约定又称为 C 调用约定,是 C/C++ 语言缺省的调用约定。


参数按照从右至左的方式入栈,函数本身不清理栈,此工作由调用者负责,返回值在eax中。


由于由调用者清理栈,所以允许可变参数函数存在

 我们可以清晰的看到 他把把返回值放在eax寄存器里面

我们在汇编代码可以清晰到是由右向左依次入栈的 ,所以我们在给默认值的时候,必须是从右向左依次给的,如果不然那函数就不知道把谁压栈。


函数调用约定有很多在此就不一一赘述

5.  函数重载

重载?啥是重载呢?就好比是一个次在不同语境下会有不同的含义,比如说 菊花,鸹貔,老司机,这一类的词,我们就说他被重载了。


5.1函数重载概念

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

#include 
using namespace std;
int Add(int left, int right)
{
	return left + right;
}

double Add(double left, double right)
{
	return left + right;
}

double Add(int left, double right)
{
	return left + right;
}

int main()
{
	cout << Add(10, 20) << endl;
	cout << Add(1.2, 3.4) << endl;
	return 0;
}

调用这Add函数每次根据传入的实参的类型不同,系统找到与之相应的入口参数进行匹配从而实现一个函数多种用途,也就是我们的重载。


所以我们根据以上例子就可以得出函数重载的条件:

  1. 必须在同一个作用域下。


  2. 函数名相同。


  3. 参数数据类型不同,个数不同,顺序不同。


函数返回值不可以作为重载条件!

5.2函数重载的原理
  1. 编译器在编译的时候,会对传递的实参类型进行推演,然后根据推演的结果选择合适的函数进行调用,如果是完全参数类型都是匹配的,则直接调用。


  2. 如果不能进行完全匹配,则编译器会进行隐式类型转换,如果转换之后还没有合适的函数进行调用则报错,反之若有合适的函数则进行调用。


 这个例子就很好的说明了发生了隐式类型转换,我传了2个参数一个int 一个 double int可以转换为double double 可以转换为int 这个时候编译器就不知道该调谁了,给你报错有两个相似的转换。


回到刚刚那个问题函数返回值不能作为重载的条件,我隐式类型转换,且匹配成功但是有两个函数返回值是不一样样,翻译器也不知调用谁。


5.3函数重载的底层实现原理

 为什么C语言不可以实现函数重载而C++可以呢?

首先我们回顾一下C和C++编译器的处理方式,预处理,编译,汇编,链接。


5.3.2 C/C++编译器的处理方式
  1. 预处理 :头文件展开,宏替换,去注释,条件编译。


  2. 编译 : 将预处理后的源文件转换为汇编语言文件,只编译源文件,不编译头文件,头文件在刚刚预处理阶段已经展开。


  3. 汇编 : 虽然叫做汇编,但是不是转变为汇编代码,而是将刚刚的汇编语言文件转换为机器码,也就是二进制文件。


  4. 链接 :将生成的二进制代码与库函数以及其他目标文件,通过链接器链接起来形成可执行文件的过程。


对于这块不熟悉可看之前的一篇文章

C语言 程序的翻译 预处理 编译 汇编 链接 #define详解

C语言编译器的处理方式

这时候光声明没定义,给你报错报的是链接错误就是你这函数没有定义,我编译器找不到他的入口地址,但是你的名字不是add吗前面这个_add 是啥。


C++编译器处理方法

我这边也没有定义, 这个?add@@YAHHH@Z  ,?add@@YAHNH@Z  ,?add@@YAHNN@Z  是啥意思呀?

说明编译器在把函数名修改了!这里我们在谈到名字修饰的问题。


5.3.3 名字修饰
  1. 编译器需保证每个函数的实体名称唯一,防止名字污染,而为每个函数名进行修饰;
  2. 编译器在链接时,当出现调用函数,就是通过修饰后的实体函数名来进行查找的,不同编译器不同语言的修饰规则不同。


C语言的修饰规则就是简单在下面加上下划线。


在MSVC编译器下修饰规则则是这样的

我们再在Linux g++ 下面看一下

Linux是开源的他的方式方法就很好让人读懂没有MSVC这么诡异

C

 

没有做啥修饰就是单纯函数名字 

C++

 对于C++就是_z3指的是函数名字占三个字符,add就是函数名字,后面俩就是参数类型,i就是int,d就是double。


 

我们在回到刚才那个问题 ,为什么有名字修饰,为什么C语言不能进行函数重载?

  1.  .c或者.cpp文件,需要在编译器经过预处理 编译 汇编 链接 生成可执行文件,才能让计算机直接运行,而 *** 作系统会在编译环节堆程序里的函数进程 名字修饰以便识别查找;
  2.   C语言的所有编译器对进程函数的名字修饰并不涉及参数,只能通过函数名对不同的函数进行区分,故C语言不支持函数重载;
  3.   C++的编译器对其函数进行的名字修饰会将参数类型包括,故C++支持函数重载
  4.  链接阶段是通过经过修饰的函数名字找他的入口地址,而修饰规则跟他参数列表的参数类型有关,跟函数返回值没有关系,因此函数返回值不可以作为重载条件。



     

5.4 extern “ C ”

假设一个情景在一个公司做开发,有人熟悉C语言,有人熟悉C++那怎么办呢?

在C++中可以使用extern “ C ”修饰一个函数,则是告诉编译器修饰函数按照C语言的风格编译.

在拿刚刚那个链接错误举例子来看。


#include
using namespace std;

extern "C" int add(int left, int right);

int main() {
	int ret = add(1, 2);

}

明显发现这是浓烈的C语言风格,函数名称前面加了个下划线来进行修饰。


 

其实extern不是这样使用的一般是我们创建库的时候使用的。


这里演示一下VS2022静态库的创建以及使用。


 

 

 

 

就可以看见是一个静态库了。


生成解决方案后就可以在目录中找到了

 

找到刚刚 工程那个文件把静态库拷贝进去

 

 

 我们在把他用起来

#include
using namespace std;
//刚刚的静态库文件
#include"testlib.h"

//预处理命令把静态库引进来
// .\是当前目录然后\转义字一下..\是当前目录的上一级目录
#pragma comment (lib,".\\..\\Debug\\lib\\testlib.lib")


int main() {
	cout << add(1, 2) << endl;
}

 我现在是C++写的静态库我C++当然可以用,那我现在创建一个C语言的工程呢?

我现在在C程序里面调add函数,就发现报错了。


这时候就该用到extern “ C ”了

#pragma once


#if __cpluscplus

extern "C"
{
#endif

	int add(int left, int right);
	int sub(int left, int right);
	int mul(int left, int right);
    int div(int left, int right);
#if __cpluscplus
}
#endif

 条件编译一下就好了。


6. 引用 6.1引用的概念

引用不是新定义的一个变量,而是给已经存在的变量起了一个别名,编译器不会给他开辟内存空间,他和他的引用变量共用一个内存空间。


张三,他也可以被叫做法外狂徒。


我们来看之前C语言这个例子。


#include
using namespace std;

//void swap(int a, int b) {
//	int temp = a;
//	a = b;
//	b = temp;
//}
void swap(int *a, int *b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}
void swap(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}
int main() {
	int a = 10;
	int b = 20;
	swap(a, b);
	cout << a << endl;
	cout << b << endl;
}

在C++里面新增了引用类型,也可以做到!

6.2引用的语法

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

void Test()
{
    int a = 10;
    int& ra = a;
    printf("%p\n", &a);
    printf("%p\n", &ra);
}
6.3引用的特性
  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体

 很明显验证了编译器不会给他开辟内存空间,他和他的引用变量共用一个内存空间。


 一个变量可以有多个引用,这几个全是一个货,地址都一样,我rrra一改全改了。


 引用一旦引用一个实体,再不能引用其他实体,后面会说。


6.4 常引用

· 在C语言里面一个变量被const修饰之后他就是一个不可以被修改的变量,而在C++里面一个变量被const修饰之后就是一个常量了,我给个数组把a丢进去也是可行的充分说明这时候a就是一个常量。


那我现在去给他来个引用,就会发现报错了!

 

再来看这个例子

 

刚刚说过引用必须引用的是一个实体跟引用实体类型相同,但是我们加const就可以,而且按理说我把a修改了ra应该也变呀,我们来看一下。


 

不仅a和ra没变,而且这个ra不是a的引用,他俩地址都不一样,说明ra就不是a的引用那是谁的引用呢?

你引用整形的,编译器就是会知道要发生隐式类型转换,编译器把12放在一个临时变量里面,你不知道变量的名字,也不知道他的地址空间,他的地址空间也没有名字,你不可以读也不可以写,说明这个东西具有常性,相当于你引用的是一个常量,那你不加const咋行呢?

6.5引用的应用 6.5.1简化代码增加可读性

还是那个swap的例子

#include
using namespace std;

void swap(int** a, int** b) {
	int* temp = *a;
	*a = *b;
	*b = temp;
}
//有了引用就可以这样做
void swap(int*& a, int*& b) {
	int* temp = a;
	a = b;
	b = temp;
}
int main() {
	int a = 10;
	int b = 20;
	int* pa = &a;
	int* pb = &b;
	swap(&pa, &pb);
	swap(pa, pb);
}

这样子代码的可读性增加了,引用其实可以代替一级指针(引用的本质......),可以吧指针简化一部分。


6.5.2增加代码的安全性

不想通过形参改变外部实参可以加一个常引用

#include 
using namespace std;
typedef struct Stack
{
	int* array;
	int size;
	int capacity;
}Stack;

// 如果引用类型作为函数的形参,尽量传递引用
// 如果不想通过形参修改外部实参,将引用给成const类型的引用
int StackSize(const Stack& s)
{
	// s.size = 100;
	return s.size;
}

int main()
{
	Stack s;

	//....进行一系列压栈 *** 作之后,栈中有5个元素
	s.size = 5;
	cout << StackSize(s) << endl;
	return 0;
}
6.5.3作为函数的返回值类型
#include 
using namespace std;
// 3. 作为函数的返回值类型
//int& Add(int left, int right)
//{
//	int temp = left + right;
//	cout << &temp << endl;
//	return temp;
//}
int temp = 0;

int& Add(int left, int right)
{
	temp = left + right;
	cout << &temp << endl;
	return temp;
}

// 注意:如果以引用方式作为函数的返回值类型,不能返回函数栈上的空间
// 如果要返回,返回的实体必须要比函数的声明周期长,即函数结束了,返回的实体依然存在
int main()
{
	int& ret = Add(10, 20);
	Add(20, 30);
	Add(30, 40);
	return 0;
}

这个例子来想一下这个ret会是多少呢?

走着

 他这个引用的是啥呀咋就给变了。


来上汇编!

 

他引用的其实是临时变量的地址。


这里就要根据汇编语言来了解函数的调用过程

 

 

当add函数结束调用后esp 和 ebp也回到原来位置,add函数对应的栈帧已经被系统回收了 ,即add对应的栈帧的空间不能使用了,但是空间还在而且add运行完成之后留下的垃圾数据也在,我的ret引用的是temp那个空间下次调用add就会把原来的垃圾数据覆盖掉,返回后空间还是在的,以此类推,编译的时候就会给你一个警告,你引用的其实是局部变量的地址,你一解引用访问的就是非法的地址!

注意:如果以引用方式作为函数的返回值类型,不能返回函数栈上的空间。



如果要返回,返回的实体必须要比函数的声明周期长,即函数结束了,返回的实体依然存在。


6.6 引用和指针的区别 6.6.1 底层实现区别
#include 
using namespace std;
 
int main() {
	int a = 10;
	int* p = &a;
	*p = 20;

	int& ra = a;
	ra = 30;
}

 

 这俩好家伙一模一样,那就是说明引用的底层实现就是按照指针的方式实现的,在底层引用就是指针常量(一旦引用一个实体,就不能引用其他实体)

引用其实是有空间的,在概念层面上说, 引用就是起别名,编译器不会为引用变量开辟新的内存空间,引用的变量和引用的实体用的是同一份空间,但是其实在底层引用是有空间的,他就是指针呀,在底层跟指针没有任何区别。


6.6.2 使用区别

1.有NULL指针但是没有NULL引用。


这个NULL就是一个宏呀,是一个常量当然不能放在引用旁边。


 

2.引用一旦引用一个实体就不能在引用其他实体。


打码机指针可以任何时间指向同一个类型的实体。


其实就是好比char * 跟 char * const 的区别

3.指针初始化没有任何要求,可以随便指,只不过我们一般让他指向NULL,但是对与引用在使用的时候必须初始化。


4.引用++是给引用后的实体++,而指针++是+上其所指向类型的大小。


5.有多级指针但是没有多级引用。


6.在sizeof里面含义不一样,sizeof(引用)结果是的引用类型的大小,sizeof(指针)在64位下是8,32位下是4。


#include 
using namespace std;
 
int main() {
	int a = 10;
	double b = 12.34;

	double* p1 = &b;
	int* p2 = &a;

	int& ra = a;
	double& rb = b;

	cout << sizeof(p1) << endl;
	cout << sizeof(p2) << endl;
	cout << sizeof(ra) << endl;
	cout << sizeof(rb) << endl;

}

 

 7. 访问实体不同,指针需要解引用,引用编译器自己处理。


7. 内联函数 7.1 #define 宏

还记得在C语言里面的宏  #define  吗?

在你的文本文件变成可执行文件编译器处理的时候,第一步就是预处理,预处理会进行,头文件展开,去注释,条件编译,和宏替换,以为是在预处理进行的替换,那他会出一些奇奇怪怪的错误。


7.1.1  宏常量

那我们一般对于数字进行宏替换的时候要加;吗?

#define MAX 1000;
#define MAX 1000

在Linux底下看看预处理完的文件

再举个例子在上文 我们刚刚谈到了const 修饰 一个变量在C++跟 C语言不同 

C语言 const修饰就是原本这个变量变成了一个不可修改的常量。


C++中const修饰就是原本这个变量变成了一个常量。


#include 
using namespace std;
 
//在C++中 const修饰的内容已经是一个常量了
// 在C语言中,const修饰的内容是一个不可以被修改的变量
int main()
{
	const int MAX_SIZE = 100;
	int array[MAX_SIZE];
	return 0;
}

按理说我通过指针已经把a的内容修改了,不是应该是100吗?为啥是10?

const修饰的内容不仅仅是一个常量,而且还具有宏替换的效果,并且替换发生在编译时。


#include 
using namespace std;
 
// const修饰的内容不仅仅是一个常量,而且还具有宏替换的效果
// 并且替换发生在编译时
int main()
{
	const int a = 10;

	int* pa = (int*)&a;  
	*pa = 100;

	cout << a << endl; 
	cout << *pa << endl;
	return 0;
}

 

7.1.2 定义宏

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏。



下面是宏的申明方式:
#define name( parament-list ) stuff其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。



注意:
参数列表的左括号必须与name紧邻。


如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分

比如这个例子

我们原本想的是应该是11*11 结果是121 为啥是21呢,那就看看.i 文件看看到底宏替换哪块出问题了 

原来是替换之后根据运算符号的先后顺序所以结果是21那么怎样去避免这个问题呢我们在宏定义的时候这样去做 

#define sq(x) (x)*(x)

 不是按理来说应该是110吗咋变成1210了那我们再看看.i文件

#define sq(x) ((x)*(x))

带参宏就会有很多很多的问题在替换中提现到那只能尽可能的加()来避免 *** 作顺序带来的问题

尽量不要使用这种带参数的

但是却省掉了很多函数调用的开销,参数压栈,开辟栈帧等一些列 *** 作,运行效率高,他就是简单的替换!

7.1.3 宏的替换规则

        1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。


如果是,它们首先被替换。



        2. 替换文本随后被插入到程序中原来文本的位置。


对于宏,参数名被他们的值替换。



        3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。


如果是,就重复上述处理过程。



注意:
        1. 宏参数和#define 定义中可以出现其他#define定义的变量。


但是对于宏,不能出现递归。



        2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。


7.1.4 宏的优缺点

优点:
        1.增强代码的复用性。



        2.提高性能。



缺点:
        1.不方便调试宏。


(因为预编译阶段进行了替换)
        2.导致代码可读性差,可维护性差,容易误用。



        3.没有类型安全的检查 。


但是宏的缺点还是很多的C++为了解决就提出了内联函数。


7.2 概念

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


#include 
using namespace std;
 
inline int Add(int left, int right)
{
	return left + right;
}


int main()
{
	int a = 10;
	int b = 20;
	int ret = Add(a, b);
	cout << ret << endl;


	return 0;
}

那他是怎样展开的呢?让我们再来看看汇编代码。


这一看没有展开呀 我call指令把这个当函数调用了,因为我们是在Debug模式下为了调试,如果展开了就不能调试了,在Debug模式下用户要进行调试。


 发现就根本没有调用函数,直接把1Eh也就是30直接给打印了,就不用调用函数,也不需要参数压栈,开辟栈帧,那一系列的 *** 作了。


那我要看展开没有那就得设置一下编译器。


 

就可以了 

 

 7.3注意事项

        1. inline是一种以空间换时间的做法,省去调用函数额开销。


所以代码很长或者有循环/递归的函数不适宜使用作为内联函数。


        2. inline对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等等,编译器优化时会忽略掉内联。


#include 
using namespace std;

inline int Add(int left, int right)
{
	Add(left,right);
	return left + right;
}


int main()
{
	int a = 10;
	int b = 20;
	int ret = Add(a, b);
	printf("&d", ret);
	return 0;
}


        3. inline不建议声明和定义分离,分离会导致链接错误。


因为inline被展开,就没有函数地址了,链接就会找不到。


持续更新中...........

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/584819.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-12
下一篇 2022-04-12

发表评论

登录后才能评论

评论列表(0条)

保存