一、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;
}