C++入门

一、C++与C语言的语法区别

C++兼容C语⾔的绝⼤多数的语法,C++中定义源文件时后缀需要改为.cpp,vs编译器看到是.cpp就会调⽤C++编译器编译(同理,vs编译器看到是.c就会调⽤C语言编译器编译),linux下要⽤g++编译,不再是gcc。

二、命名空间

cpp 复制代码
#include <stdio.h>                                                             
#include <stdlib.h>//头文件中的内容将会在该源文件中全部展开
int rand = 10;
int main()
{
	//编译报错:error C2365 : "rand":重定义;以前的定义是"函数"
	printf("%d\n", rand);//定义的变量rand与头文件<stdlib.h>中的rand函数都在全局域中,且名字相同,出现了命名冲突,导致编译器不知道打印哪一个rand
	return 0;
}

那么该如何解决这种情况呢?请看下面的代码:

cpp 复制代码
#include <stdio.h>                                                             
#include <stdlib.h>
namespace bit
{
	int rand = 10;
}
int main()
{
	printf("%p\n", rand);//编译器先去局部域中找rand,发现没有,接着去全局域中找,发现了rand函数,因此打印rand函数的地址(函数名是函数的地址)
	//%p是以16进制的形式打印地址
	printf("%d\n", bit::rand);//编译器会直接去bit域中找rand
	return 0;
}

2.1:namespace

a: 定义命名空间时,需要⽤到关键字namespace,后⾯加命名空间的名字,然后接⼀对{}即可,{}后不需要加英文的分号,{}中包含命名空间的成员。命名空间中可以定义变量/函数/类型等。
cpp 复制代码
namespace wangbo
{
	int a = 5;//定义变量
	int Add(int x, int y)//定义函数
	{
		return x + y;
	}
	struct student//定义结构体类型
	{
		int age;
		int score;
	};
}

int main()
{
	printf("%d\n", wangbo::a);
	printf("%d\n", wangbo::Add(1, 1));
	wangbo:: student s = { 18,95 };//C++中使用结构体类型创建变量时,可以省略关键字struct
	return 0;
}
b: namespace本质是定义出⼀个域,这个域跟全局域各⾃独⽴,不同的域中可以定义同名变量、同名函数...(同一个域中不能定义同名变量、同名函数...)
c: C++中域包括局部域,全局域,命名空间域和类域;域会影响代码编译时,编译器查找⼀个变量/函数/类型的出处(声明或定义)。当指定了域时, 编译器会直接去指定域中找,如果没找到就会报错;当没有指定域时,编译器会先去局部域中找,如果没找到再去全局域中找,如果还没找到就会报错。

另外,局部域和全局域除了会影响编译查找逻辑,还会影响变量的⽣命周期(局部域中,变量的生命周期就是局部范围内,全局域中,变量的生命周期与整个工程(项目)的生命周期一致),命名空间域和类域不影响变量的⽣命周期。

cpp 复制代码
// 变量a其实也算一个全局变量,只不过套了一个命名空间,这个命名空间是为了防止命名冲突的,不会影响它的生命周期。
namespace wangbo
{
	int a = 5;
	int Add(int x, int y)
	{
		return x + y;
	}
	struct student
	{
		int age;
		int score;
	};
}
d: namespace只能定义在全局,另外命名空间还可以嵌套定义。
cpp 复制代码
//假设鹏哥与航哥在同一个命名空间下,都想定义一个名为a的变量和一个名为Add的函数,为了避免命名冲突,该怎么办呢?
//可以使用命名空间嵌套
namespace bit
{
	namespace pg
	{
		int a = 0;
		int Add(int x, int y)
		{
			return x + y;
		}
	}
	namespace hg
	{
		int a = 1;
		int Add(int x, int y)
		{
			return x + y;
		}
	}
}

int main()
{
	printf("%d\n", bit::pg::a);//0
	printf("%d\n", bit::hg::a);//1

	printf("%d\n", bit::pg::Add(1, 2));//3
	printf("%d\n", bit::pg::Add(1, 0));//1
	return 0;
}
e: 一个工程中,可以在多个文件中定义同名的命名空间(namespace),同名的命名空间中的内容会默认合并到一起,就像同一个命名空间一样。
cpp 复制代码
// Add.h
#pragma once
namespace bit
{
	int Add(int x, int y);
}
cpp 复制代码
// Add.cpp
#include"Add.h"
namespace bit
{
	int Add(int x, int y)
	{
		return x + y;
	}
}
cpp 复制代码
// Sub.h
#pragma once
namespace bit
{
	int Sub(int x, int y);
}
cpp 复制代码
// Sub.cpp
#include"Sub.h"
namespace bit
{
	int Sub(int x, int y)
	{
		return x - y;
	}
}
cpp 复制代码
// Test.cpp
#include"Add.h"
#include"Sub.h"
#include<stdio.h>
int main()
{
	printf("%d\n", bit::Add(1, 2));// 3
	printf("%d\n", bit::Sub(1, 2));// -1
	return 0;
}
f: C++标准库中的内容都放在一个叫std(standard)的命名空间中。
补充: 域作用限定符 ::
cpp 复制代码
#include <stdio.h>                                                             
int x = 0;//这个x是在全局域中,属于全局变量
int main()
{
	int x = 1;//这个x是在局部域中,属于局部变量
	printf("%d\n", x);//1   编译器会先去局部域中去找有没有x,找到了的话就打印x;若没有找到再去全局域中去找x
	printf("%d\n", ::x);//0  ::是域作用限定符,当::什么都不写时,编译器会直接去全局域中找x
	return 0;
}

2.2:命名空间的使用

编译查找一个变量的声明/定义时,默认会先去局部域找,再去全局域查找,但不会到命名空间里面去查找。所以下面程序会编译报错。

cpp 复制代码
#include<stdio.h>
namespace bit
{
int a = 0;
int b = 1;
}
int main()
{
// 编译报错:error C2065: "a": 未声明的标识符
printf("%d\n", a);
return 0;
}

我们要使用命名空间中定义的变量/函数,有三种方式:

1.指定命名空间访问,项目中推荐这种方式。但每次使用命名空间中的内容时,都需要使用 :: 指定命名空间,比较麻烦。
cpp 复制代码
namespace bit
{
	int a = 0;
	int b = 1;
}

// 指定命名空间访问
int main()
{
	printf("%d\n", bit::a);
	return 0;
}
2只引入命名空间域中的单个名称(将单个名称引入当前作用域中,即using这句代码所在的作用域)
cpp 复制代码
namespace bit
{
	//命名空间域bit中有a、b两个变量
	int a = 0;
	int b = 1;
}
int b = 3;//全局域中有变量b
int main()
{
	using bit::b;//将bit域中的b引入main函数的作用域(局部域)中
	printf("%d\n", b);// 1  编译器既能看到局部域中的b,又能看到全局域中的b。根据局部域与全局域的名称相同时,局部名称会屏蔽全局名称的规则,编译器将打印局部域中的b
	return 0;
}
cpp 复制代码
namespace bit
{
	int a = 0;
	int b = 1;
}
using bit::b;//将bit命名空间中的b引入到全局域中
int main()
{
	printf("%d\n", bit::a);//0
	printf("%d\n", b);// 1
	return 0;
}
cpp 复制代码
namespace bit
{
	int a = 0;
	int b = 1;
}
int b = 3;
int main()
{
	using bit::b;// 将bit命名空间中的b引入到main函数的函数域(局部域)中
	printf("%d\n", b);// 1  编译器既能看到局部域中的b,又能看到全局域中的b,根据局部域与全局域中的名称相同时,局部名称会屏蔽全局名称的规则,编译器将打印局部域中的b
	return 0;
}
3.引入整个命名空间域(当前作用域中可以看到该命名空间域中的内容)。在大型项目中不推荐这种方法,容易发生命名冲突。在日常小练习时,为了图个方便可以这样用。
cpp 复制代码
namespace bit
{
	//命名空间域bit中有a、b两个变量
	int a = 0;
	int b = 1;
}
int a = 3;//全局域中有a、b两个变量
int b = 5;
int main()
{
	using namespace bit;//将命名空间域引入main函数的作用域。也就是说在main函数的作用域中可以看到bit域的内容
	printf("%d\n", a);//编译器既能看到全局域中的a,也能看到bit域的a,编译器不知道打印哪个a,因此会报错
	return 0;
}

注意与下面这段代码区分开

cpp 复制代码
int a = 3;//全局域中有a、b两个变量
int b = 5;
int main()
{
	//main函数的作用域(局部域)中定义了a、b两个变量
	int a = 1;
	int b = 2;

	//当局部域与全局域中的名字冲突时,局部域的名字将屏蔽全局域的名字,因此编译器将打印局部域中的a
	printf("%d\n", a);//1
	return 0;
}
cpp 复制代码
namespace bit
{
	int a = 0;
	int b = 1;
}
using namespace bit;//将命名空间域bit引入到全局域中。也就是说在全局域中,可以看到bit域中的内容
int main()
{
	printf("%d\n", a);//0
	printf("%d\n", b);//1
	return 0;
}

三、C++中的输入与输出

1.头文件 < iostream > 是 Input Output Stream 的缩写,是标准的输入、输出流库,定义了标准的输入、输出对象。
2.std::cin 是 istream 类的对象,它主要面向窄字符(narrow characters (of type char))的标准输入流。
3.std::cout 是 ostream 类的对象,它主要面向窄字符的标准输出流。
4.std::endl 是一个函数,流插入输出时,相当于插入一个换行字符加刷新缓冲区。
5.<<是流插入运算符,>>是流提取运算符。(C语言还用这两个运算符做位运算左移/右移,C++中也支持这种语法)
cpp 复制代码
#include<stdio.h>
#include<iostream>
int main()
{
	std::cout << (1 << 1) << std::endl;// 2
	return 0;
}
6.C++中的cin / cout比c语言中的scanf / printf更方便,不需要scanf / printf输入输出时那样,需要手动指定格式,C++的输入/输出可以自动识别变量类型(本质是通过函数重载实现的,这个以后会讲到)。
cpp 复制代码
#include <iostream>
using namespace std;
int main()
{
	int a = 0;
	double b = 0.1;
	char c = 'x';
	cout << a << " " << b << " " << c << endl;
	// 等价于
	/*
	cout << a
	cout<<" "
	cout << b
	cout<<" "
	cout << c
	cout<<endl
	*/

	// 可以自动识别变量的类型
	cin >> a >> b >> c;
	cout << a <<" " << b << " " << c << endl;
	return 0;
}
7.cin / cout可以输入/输出任意类型的数据(比如string类型),但scanf与printf只能输入/输出内置类型的数据(比如int、double等),比如不能针对string类型的数据。
8.需要进行大量的输入/输出时,cin / cout的效率是不如scanf / printf的。此时要么使用scanf / printf,要么在使用cin/cout前加上下面这几行代码。
cpp 复制代码
int main()
{
// 在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下3行代码
// 可以提高C++IO效率
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
9.IO流涉及类和对象,运算符重载、继承等很多面向对象的知识,这些知识我们还没有讲解,所以这里我们只能简单认识一下C++ 中IO流的用法,后面我们会有专门的一个章节来细节IO流库。
10.cout/cin/endl等都属于C++标准库,C++标准库都放在一个叫std(standard)的命名空间中,所以要通过命名空间的使用方式去用它们。
11.一般日常练习中我们可以using namespace std,实际项目开发中不建议using namespace std,容易发生命名冲突。
12.VS编译器中,头文件 < iostream > 中默认包含了头文件<stdio.h>,但其他的编译器中不一定会包含<stdio.h>

四、缺省参数

1.缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参,则采用该形参的缺省值,否则使用指定的实参,缺省参数分为全缺省和半缺省参数。(有些地方把缺省参数也叫默认参数)
2.全缺省就是全部形参给缺省值,半缺省就是部分形参给缺省值。C++规定半缺省参数必须从右往左 依次连续缺省,不能间隔跳跃给缺省值。
3.调用带缺省参数的函数时,C++规定必须从左到右依次给实参,不能跳跃给实参。
cpp 复制代码
#include <iostream>
using namespace std;
void Func(int a = 0)
{
	cout << a << endl;
}
int main()
{
	Func(); // 没有传参时,使用参数的默认值
	Func(10); // 传参时,使用指定的实参
	return 0;
}
cpp 复制代码
#include <iostream>
using namespace std;
// 全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}
// 半缺省
void Func2(int a, int b = 10, int c = 20)
{
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "c = " << c << endl << endl;
}
int main()
{
	Func1();
	Func1(1);
	Func1(1, 2);
	Func1(1, 2, 3);
	//Func2();  error
	Func2(100);
	Func2(100, 200);
	Func2(100, 200, 300);
	return 0;
}
4.函数声明和定义分离时(分离是指函数的声明放在头文件.h中,函数的实现放在源文件.cpp中),缺省参数不能在函数声明和定义中同时出现,必须在函数声明时给缺省值。
cpp 复制代码
// Stack.h
#include <iostream>
#include <assert.h>
using namespace std;
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps, int n = 4);//在函数声明时给缺省值
cpp 复制代码
// Stack.cpp
#include"Stack.h"
// 缺省参数不能声明和定义同时给。(为了防止函数声明与定义时,缺省参数的值给的不一样)
void STInit(ST* ps, int n)
{
assert(ps && n > 0);
ps->a = (STDataType*)malloc(n * sizeof(STDataType));
ps->top = 0;
ps->capacity = n;
}
cpp 复制代码
// test.cpp
#include"Stack.h"
int main()
{
// 不确定要插入多少个数据,默认开辟4个空间
ST s1;
STInit(&s1);

// 确定要插入1000个数据,初始化时一把开好,避免频繁扩容,影响程序的性能
ST s2;
STInit(&s2, 1000);
return 0;
}

五、函数重载

C++支持在同一作用域中出现同名函数,但要求这些同名函数的形参不同。可以是形参个数不同或者类型不同。这样调用函数时就表现出了多态行为,使用更灵活。C语言是不支持同一作用域中出现同名函数的。

cpp 复制代码
#include<iostream>
using namespace std;
// 1、参数类型不同
int Add(int left, int right)
{
	return left + right;
}
double Add(double left, double right)
{
	return left + right;
}

int main()
{
	int a = 1, b = 2;
	double c = 2.1, d = 5.1;
	cout << Add(a, b) << endl;// 3 
	cout << Add(c, d) << endl;// 7.2
	return 0;
}
cpp 复制代码
// 2、参数个数不同
#include<iostream>
using namespace std;
void f()
{
	cout << "f()" << endl;
}
void f(int a)
{
	cout << "f(int a)" << endl;
}

int main()
{
	f();// f()
	f(1);// f(int a)
	return 0;
}


六、引用

6.1:什么是引用?

引用不是新定义一个变量,而是给已存在变量取了一个别名,它俩共用同一块内存空间。比如:水浒传中的李逵,宋江叫他"铁牛",江湖上的人称他为"黑旋风",李逵有几个别名,但都是指同一个人。

语法:引用对象的类型& 引用对象的别名 = 引用对象;

cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int a = 3;
	int& b = a;// 给变量a取了个别名b
	int& c = a;// 又给变量a取了个别名c
	int& d = c;// 给变量c取了个别名d
	cout << &a << endl;  // 这里的&表示取地址
	cout << &b << endl;
	cout << &c << endl;
	cout << &d << endl;
	//都是打印006FFACC
	return 0;
}
cpp 复制代码
#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	int& b = a;
	int c = 20;
	// 这里并非让b引用c,因为引用不能改变指向。而是将c的值赋给b
	b = c;
	return 0;
}
补充 :

C++中为了避免引入太多的符号,会使用用C语言的一些符号,比如前面的<< 和 >>,在不同的场景下,可能表示移位运算符,也可能表示流插入运算符 / 流提取运算符,这里引用也和取地址也使用了同一个符号&,我们在不同的场景下能够区分就行。(吐槽一下,这个问题其实挺坑的,个

人觉得用更多符号反而更好,不容易混淆)

6.2:使用引用时的注意事项

6.2.1:引用在定义时必须初始化
6.2.2: 一个变量可以有多个引用(即多个别名)
6.2.3:引用一旦引用一个实体,再不能引用其他实体。

6.3:引用的使用

6.3.1:引用的功能

引用在实践中主要是引用传参和引用作为函数的返回值,可以减少拷贝提高效率。

引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。

cpp 复制代码
#include<iostream>
using namespace std;
void Swap(int& p, int& q)//p是x的别名,q是y的别名
{
	int tmp = p;
	p = q;
	q = tmp;
}
void Swap(int* x, int* y)
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}

int main()
{
	int x = 0, y = 1;
	Swap(x, y);//引用传参
	cout << x << " " << y << endl;// 1 0
	Swap(&x, &y);//传址调用
	cout << x << " " << y << endl;// 0 1
	//若希望形参值的改变影响实参的值,既可以采用引用传参,也可以采用传址调用
	return 0;
}
cpp 复制代码
// 若想交换两个指针变量的值,如果采用传址调用,那么函数的形参就需要设计成二级指针,比较麻烦。
// 我们可以通过引用传递的方式,交换两个指针变量的值
void Swap(int*& m, int*& n)//m是pa的别名,n是pb的别名
{
	int* temp = m;
	m = n;
	n = temp;
}
int main()
{
	int a = 0, b = 1;
	int* pa = &a;
	int* pb = &b;
	Swap(pa, pb);
	return 0;
}
cpp 复制代码
// 一些主要用C代码实现版本数据结构教材中,使用C++中的引用替代指针传参,目的是简化程序,避免使用复杂的指针,但是很多同学没学过引用,导致一头雾水。
#include<stdlib.h>
typedef struct ListNode
{
	int val;
	struct ListNode* next;
}LTNode, * PNode;
/*
上面的typedef等价于:
typedef struct ListNode  LTNode;
typedef struct ListNode*  PNode;
*/

// 指针变量也可以取别名,下面的形参LTNode*& phead就是给指针变量取别名
// 形参使用引用的方式,就不需要用二级指针了,相对而言简化了程序
//void ListPushBack(LTNode** phead, int x)
//void ListPushBack(LTNode*& phead, int x)
void ListPushBack(PNode& phead, int x)//phead是plist的别名
{
	PNode newnode = (PNode)malloc(sizeof(LTNode));
	newnode->val = x;
	newnode->next = NULL;
	if (phead == NULL)
	{
		phead = newnode;
	}
	else
	{
		//...
	}
}

int main()
{
	PNode plist = NULL;
	ListPushBack(plist, 1);
	return 0;
}
补充:传值返回与引用返回的区别

1.传值返回时,会将返回的值拷贝到一个临时变量中(比如拷贝到寄存器中),再将拷贝的值给返回。这个临时变量具有常性(类似于被const修饰的变量一样,它的值不能被直接修改)

2.引用返回时,会为返回的对象创建一个引用对象(两个对象共用同一块存储空间),也就是给返回的对象取一个别名,真正返回的是这个别名。

3.也就是说,引用作为函数的返回值类型时,可以修改返回的对象的值,但是传值返回时,不能修改函数的返回值。

cpp 复制代码
#include<assert.h>
#include<iostream>
using namespace std;
//定义动态顺序表的结构
typedef struct SeqList
{
	int* a;// 动态顺序表空间的起始地址
	int size;// 动态顺序表中有效数据个数
	int capacity;// 动态顺序表的容量(最多能存储多少个有效数据)
}SL;

void SLInit(SL& s1, int n = 4)
{
	s1.a = (int*)malloc(n * sizeof(int));
	s1.size = 0;
	s1.capacity = n;
}

void SLPushBack(SL& s1, int x)
{
	//....检查空间足够
	s1.a[s1.size] = x;
	s1.size++;
}

//返回动态顺序表中第i个有效数据
int SLAt1(SL& s1,int i)
{
	assert(i < s1.size);
	return s1.a[i];
}

int& SLAt2(SL& s1, int i)
{
	assert(i < s1.size);
	return s1.a[i];
}

int main()
{
	SL s;
	SLInit(s);//默认开辟4个空间
	SLPushBack(s, 1);
	SLPushBack(s, 2);
	SLPushBack(s, 3);
	SLPushBack(s, 4);
	//for (int i = 0; i < 4; i++)
	//{
	//	SLAt1(s, i) += 1;// error:
	//}
	/*
	传值返回时,会将s1.a[i]的值拷贝到一个临时变量中(比如拷贝到寄存器中),这个临时变量具有常性(它的值不能被直接修改)
	因此编译器会报错
	*/


	for (int i = 0; i < 4; i++)
	{
		SLAt2(s, i) += 1;
	}
	/*
	引用返回时,会为返回的对象s1.a[i]创建一个引用对象,比如创建引用对象叫tmp
	int& tmp = s1.a[i] (tmp与s1.a[i]共用同一块存储空间)
	然后返回tmp
	*/

	for (int i = 0; i < 4; i++)
	{
		//引用返回时,可以修改返回的对象的值,但传值返回时不能
		cout << SLAt2(s, i) << " ";//2 3 4 5
	}
	return 0;
}
补充:引用作为函数的返回值类型一定合理吗?

不一定,比如当返回局部变量时,引用就不适合作为函数的返回值类型。

cpp 复制代码
#include<iostream>
using namespace std;
int& fun()
{
	int ret = 1;
	//....
	return ret;
}
/*
ret是个局部变量,fun函数在返回时,会为ret创建一个引用对象,假设引用对象的名字叫tmp
int& tmp = ret  (ret与tmp共用一块存储空间)
当fun函数的函数栈帧销毁后,ret的空间就不属于当前的程序了,但通过tmp,将这块空间中的值赋给了x(这是一种类似于野指针的行为)
显然是不合理的
*/

int main()
{
	int x = fun();
	return  0;
}

七、inline(知识点很深入)

7.1:C语⾔中的宏函数在预处理时会进行相应的替换,但是写一个正确的宏函数是比较麻烦的,很容易写错,且宏是不能调试的,于是C++就设计了inline来替代C的宏函数。

7.2:inline修饰的函数叫做内联函数。编译时,会在调用的地方展开内联函数,不需要建立函数栈帧,可以提高程序的效率。

7.3:vs编译器的debug版本中,调用内联函数时,默认是不展开的,而是建立函数栈帧,若想在debug版本中展开,需要设置以下两个地方。(release版本下,调用内联函数时一般会展开)

7.4: inline对于编译器⽽⾔只是⼀个建议。也就是说,加不加inline是程序员的事,调用时是否会展开由编译器决定。(对于同一个内联函数,在不同的编译器下调用时,可能会展开,可能不会展开。)因为C++标准没有规定这个。对于需要频繁调⽤且函数体中的代码量较少的函数(比如5行左右),建议用inline修饰。对于递归函数(当递归的层次比较深时,如果编译器选择了展开内联函数,编译阶段将会额外产生大量的指令,会使得程序的性能下降,最终生成的可执行程序变得很大,编译器不会允许这种情况发生)或函数体中的代码量较多的函数,加上inline后,调用时,编译器也不会展开。

7.5:调用内联函数时,如果内联函数展开了,就无法进入函数内部调试。

补充1:内联函数的信息不会进入符号表。

7.6:内联函数与static修饰的函数不能将函数的声明和定义分离到两个文件(函数的声明放在.h文件,函数的定义放在.cpp文件),一旦分离会导致链接错误。正确的做法是直接将内联函数的定义写在.h文件中。

错误的写法:

cpp 复制代码
// Add.h
#pragma once
inline int Add(int x, int y);
cpp 复制代码
// Add.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"
inline int Add(int x, int y)
{
	return x + y;
}
cpp 复制代码
// Test.cpp
#include"Add.h"
int main()
{
	int ret = Add(2, 3);
	cout << ret << endl;
	return 0;
}


上面是一个内联函数的声明与定义分离的例子。
链接阶段会有两个文件(Add.o与Test.o)
Test.o中只有Add函数的声明,因此编译器要去Add.o文件的符号表中找Add函数的地址,以及与Add函数相关的信息,但糟糕的是内联函数的信息不会进入符号表,导致Test.o文件中找不到Add函数的地址,所以会产生链接错误。

正确的写法:

cpp 复制代码
// Add.h
#pragma once
inline int Add(int x, int y)
{
	return x + y;
}
cpp 复制代码
// Test.cpp
#include"Add.h"
int main()
{
	int ret = Add(2, 3);
	cout << ret << endl;
	return 0;
}

预处理阶段,Add函数的定义就会展开到Test.cpp中。调用Add函数时,Add函数体中的逻辑就会在调用的地方展开。

补充2:为什么普通函数(没有用static或inline修饰的函数)的声明与定义要分离呢(函数的声明放在.h文件中,函数的实现放在.cpp文件中)
cpp 复制代码
// Add.h
#pragma once
int Add(int x, int y);
cpp 复制代码
// Add.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"
int Add(int x, int y)
{
	return x + y;
}
cpp 复制代码
// Test.cpp
#include"Add.h"
int main()
{
	int ret = Add(2, 3);
	cout << ret << endl;
	return 0;
}

上面是一个非内联函数的声明与定义分离的例子。
链接阶段会有两个文件(Add.o与Test.o)
Test.o中只有Add函数的声明,因此要去Add.o文件的符号表中找Add函数的地址,以及与Add函数相关的信息,找到后就链接成功了。
假设Add.cpp文件中没有实现Add函数,那么在链接阶段,编译器在Add.o文件的符号表中找不到Add函数的地址,以及与Add函数相关的信息,会导致链接错误,无法生成可执行程序。

补充3:普通函数(没有用static或inline修饰的函数)不能在.h文件中写函数的定义,否则当多个.cpp文件包含这个头文件时,会导致重定义,产生链接错误
cpp 复制代码
// Add.h
#pragma once
int Add(int x, int y)
{
	return x + y;
}
cpp 复制代码
// Test1.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"
int main()
{
	int ret = Add(2, 3);
	cout << ret << endl;
	return 0;
}
cpp 复制代码
// Test2.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"

上述例子中,在链接阶段会有两个文件(Test1.o与Test2.o)
由于Test1.o与Test2.o文件中均有Add函数的实现,那么在这两个文件的符号表中都有Add函数的地址,以及与Add函数相关的其他信息,导致了重定义,产生了链接错误。

补充4:为什么在.h文件中定义了内联函数后,当多个.cpp文件包含该头文件时,不会产生链接错误呢?
cpp 复制代码
// Add.h
#pragma once
inline int Add(int x, int y)
{
	return x + y;
}
cpp 复制代码
// Test1.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"
int main()
{
	int ret = Add(2, 3);
	cout << ret << endl;
	return 0;
}
cpp 复制代码
// Test2.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"

上述例子中,在链接阶段会有两个文件(Test1.o与Test2.o)
由于Test1.o与Test2.o文件中均有Add函数的实现,由于内联函数的信息不会进入这两个文件的符号表,因此在链接阶段,不会产生链接错误。

补充5:static修饰的函数的信息也不会进入符号表
补充6:为什么在.h文件中定义了static修饰的函数后,当多个.cpp文件包含该头文件时,不会产生链接错误呢?
cpp 复制代码
// Add.h
#pragma once
static int Add(int x, int y)
{
	return x + y;
}
cpp 复制代码
// Test1.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"
int main()
{
	int ret = Add(2, 3);
	cout << ret << endl;
	return 0;
}
cpp 复制代码
// Test2.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"Add.h"

上述例子中,在链接阶段会有两个文件(Test1.o与Test2.o)
Test1.o与Test2.o文件中均有Add函数的实现。由于Add函数被static修饰了,使得Add函数的外部链接属性变成了内部链接属性,也就是说Add函数的信息不会进入这两个文件的符号表,因此在链接阶段,不会产生链接错误。

八、nullptr

8.1:NULL在C语言与C++中的区别

8.3:void*指针的缺陷:

void类型的指针可以接收任意类型的地址,但是void类型不能隐式类型转换为其他的指针类型。(void*类型可以强制类型转换为其他的指针类型)

cpp 复制代码
#include<iostream>
using namespace std;
void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* ptr)" << endl;
}
int main()
{
	f(0); // f(int x)
	f(NULL); // f(int x)
	f((int*)NULL); // f(int* ptr)  
	// 将整型0作为地址,强制类型转换为int*类型
	// f((void*)NULL); // error
	// 将整型0作为地址,强制类型转换为void*类型,但由于void*不能隐式类型转换为int*,因此程序会报错
	return 0;
}

8.3:nullptr的介绍:

C++11中引⼊nullptr,nullptr是⼀个特殊的关键字,nullptr是⼀种特殊类型的字⾯量,它的值是0,它可以隐式类型转换成任意其他类型的指针类型。 使⽤nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,⽽不能被转换为整数类型。

cpp 复制代码
void f(int x)
{
	cout << "f(int x)" << endl;
}
void f(int* ptr)
{
	cout << "f(int* ptr)" << endl;
}
int main()
{
	f(nullptr);// f(int* ptr)   nullptr可以隐式类型转换为任意的指针类型
	return 0;
}
相关推荐
泽虞3 小时前
《C++程序设计》笔记p4
linux·开发语言·c++·笔记·算法
什么半岛铁盒3 小时前
C++项目:仿muduo库高并发服务器--------Any类的实现
linux·服务器·数据库·c++·mysql·github
玖笙&3 小时前
✨WPF编程基础【1.1】:XAML文档框架
c++·visualstudio·wpf
数据智能老司机3 小时前
用 C/C++ 从零实现 Redis——简介
c++·redis
RIDDLE!4 小时前
Visual Studio使用C++配置OpenCV环境,同时添加模板以4.12为例
c++·opencv·visual studio
企鹅虎4 小时前
英雄C++入门到精通
c++
小虎l4 小时前
英雄C++入门到精通
c++
青草地溪水旁4 小时前
设计模式(C++)详解——解释器模式(2)
c++·设计模式·解释器模式
Kevinhbr4 小时前
CSP-J/S初赛赛后总结
c++·程序人生·ccf csp-j/s