引言
C++是在C的基础之上,容纳进去了面向对象编程思想,并增加了许多有用的库,以及编程范式等。熟悉C语言之后,对C++学习有一定的帮助,本章节主要目标:
1. 补充C语言语法的不足,以及C++是如何对C语言设计不合理的地方进行优化的,比如:作用域方面、IO方面、函数方面、指针方面、宏方面等。
2. 为后续类和对象学习打基础。
命名空间
在C++中,命名空间是一种封装名字的方式,用来解决命名冲突问题。它可以将一组名字(变量、函数、类型等)封装在一个域内,从而避免与其他名字发生冲突。
命名冲突
1.我们跟库冲突:当我们使用的名字与库中的名字相同时,会发生冲突。
2.我们互相之间冲突:在大型项目中,不同模块之间可能会使用相同的名字,导致冲突。
命名空间域与其他域
域分为类域、命名空间域、局部域、全局域。命名空间域是通过命名空间引入的一种新的域。
- ::为域作用限定符,用于访问不同域中的名字。如::a访问全局域的a,lynn::a访问命名空间lynn中的a。
命名空间的定义与使用
命名空间通过namespace关键字来定义,可以包含变量、函数、类型等。
代码示例:
#include <cstdio>// 定义一个命名空间lynn
namespace lynn {int a = 10;void printA() {printf("lynn::a = %d\n", a);}
}// 全局域中的a
int a = 20;void printA() {printf("global a = %d\n", a);
}int main() {// 访问全局域的aprintf("Global a: %d\n", ::a);// 访问命名空间bit中的aprintf("Namespace lynn a: %d\n", lynn::a);// 调用全局域的printA函数::printA();// 调用命名空间lynn中的printA函数lynn::printA();return 0;
}
输出:
展开命名空间
using namespace lynn;
编译时会去命名空间lynn中搜索名字。
代码示例:
#include <cstdio>namespace lynn {int a = 10;void printA() {printf("lynn::a = %d\n", a);}
}using namespace lynn;int main() {// 由于展开了命名空间lynn,可以直接访问a和printAprintf("a = %d\n", a);printA();return 0;
}
输出:
展开了命名空间lynn后,a相当于暴露在了全局。
搜索顺序
局部域 → 全局域 → 展开了的命名空间域→指定访问的命名空间域
命名空间的嵌套与合并
命名空间可以嵌套,这允许我们创建更复杂的命名空间结构。这对于定义自己的库特别有用,可以避免与C++标准库或其他库发生冲突。
多个同名的命名空间会被合并,这使得我们可以在不同的文件中定义同名的命名空间,而不会发生冲突。
代码示例:
#include <cstdio>namespace outer {namespace inner {int x = 100;}
}// 在另一个文件中,我们可以继续扩展outer命名空间
namespace outer {int y = 200;
}int main() {printf("outer::inner::x = %d\n",outer::inner::x);printf("outer::y = %d\n",outer::y);return 0;
}
输出:
输入和输出
在C++中,输入输出流是通过<<(流插入运算符)和>>(流提取运算符)来实现的。这两个运算符能够自动识别操作对象的类型,并进行相应的处理。
- 流插入运算符<<:用于将数据插入到输出流中,如std::cout。
- 流提取运算符>>:用于从输入流中提取数据,如std::cin。
在这里,std::是命名空间前缀,它指定了cout和cin是定义在std命名空间中的。这是为了避免命名冲突,并确保我们使用的是标准库中的cout。
不建议在项目中展开std命名空间
容易命名冲突:如果我们在项目中定义的跟std命名空间里面定义的重名,就会报错。所以不建议在项目里展,日常练习可以展开。
项目里面更推荐指定访问,还可以把常用的展开,例如using std::cout; using std::endl;
#include <iostream>
using std::cout;
using std::endl;int main() {double b = 3.14;// 使用C++的输入输出流cout << "b = " << b << endl;return 0;
}
精度丢失问题
流插入运算符可以自动识别插入数据的类型,但在自动识别类型时可能会导致小数精度丢失,因此在需要精确控制小数位数时,推荐使用C语言风格的输出方式printf。
代码示例:
#include <cstdio>
#include <iostream>
using namespace std;int main() {int a = 5;double b = 3.1415926;// 使用C++的输入输出流cout << "a = " << a << endl;cout << "b = " << b << endl;// 使用C语言的输入输出(需要包含<cstdio>)printf("a = %d\n", a);printf("b = %.7f\n", b); // 精确到小数点后5位return 0;
}
输出:
C++的cout和C语言的printf
虽然C++的cout提供了类型安全、易于扩展等优点,但在某些情况下,C语言的printf可能会更快,因为它直接操作底层的输出缓冲区,而cout则涉及更多的抽象和类型检查。
解决办法:关闭同步流
std::cout默认与C语言的stdio库保持同步,这意味着每次使用std::cout进行输出时,程序都需要检查stdio库的缓冲区状态,这会增加额外的开销。我们可以通过调用std::ios_base::sync_with_stdio(false);来关闭这种同步,从而提高std::cout的效率。
#include <iostream>
using namespace std;int main() {ios_base::sync_with_stdio(false);cout << "Hello, World!" << endl;return 0;
}
缺省参数
在C++中,缺省参数允许我们在函数调用时省略某些参数,这些被省略的参数将使用预先定义的默认值。缺省参数的使用可以大大提高代码的灵活性和可读性。以下是关于缺省参数的详细讲解和代码例子。
缺省参数的基本概念
缺省参数是在函数声明或定义中,为某些参数指定默认值。当函数调用时,如果没有提供这些参数的值,那么就会使用这些默认值。
全缺省参数
全缺省参数指的是函数的所有参数都有默认值。在调用这样的函数时,可以选择不提供任何参数,函数将使用所有参数的默认值。
代码例子
#include <iostream>void printValues(int a = 1, int b = 2, int c = 3) {std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}int main() {printValues(); // 使用所有缺省值:a: 1, b: 2, c: 3printValues(4); // a: 4, b: 2, c: 3printValues(4, 5); // a: 4, b: 5, c: 3printValues(4, 5, 6); // a: 4, b: 5, c: 6return 0;
}
半缺省参数
半缺省参数指的是函数的部分参数有默认值,而其他参数没有。在调用这样的函数时,必须提供没有默认值的参数,而可以选择省略有默认值的参数。
注意:缺省参数必须从右往左给出,不能隔着给。
代码例子
#include <iostream>void printValues(int a, int b = 2, int c = 3) {std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}int main() {printValues(1); // a: 1, b: 2, c: 3printValues(1, 4); // a: 1, b: 4, c: 3printValues(1, 4, 5); // a: 1, b: 4, c: 5// printValues(); // 错误:缺少非缺省参数areturn 0;
}
声明和定义中的缺省参数
在C++中,函数的声明和定义通常分开进行,特别是在使用头文件(.h)和源文件(.cpp)的情况下。需要注意的是,缺省参数只能在函数的声明(通常在头文件中)中给出,而不能在函数的定义(通常在源文件中)中再次给出。
代码例子:
头文件:
#ifndef EXAMPLE_H
#define EXAMPLE_Hvoid printValues(int a = 1, int b = 2, int c = 3);#endif // EXAMPLE_H
源文件:
#include <iostream>
#include "example.h"void printValues(int a, int b, int c) {std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}int main() {printValues(); // 使用缺省值:a: 1, b: 2, c: 3return 0;
}
在这个例子中,缺省参数是在头文件的函数声明中给出的,而在源文件的函数定义中没有再次给出。这是正确的做法,因为如果在定义中也给出缺省参数,将会导致编译错误。
函数重载
函数重载的概念
函数重载是指在同一作用域内,允许创建多个函数,它们具有相同的函数名,但具有不同的参数列表(参数个数、参数类型或参数类型顺序不同)。编译器会根据调用时提供的参数来匹配最合适的函数进行调用。函数重载提高了代码的复用性和可读性。
函数重载的不同形式
1.参数类型不同
当函数名相同,但参数类型不同时,可以构成函数重载。例如:
void print(int i) {cout << "Integer: " << i << endl;
}void print(double d) {cout << "Double: " << d << endl;
}
调用print(5)会匹配到第一个函数,而print(5.5)会匹配到第二个函数。
注意:函数名相同,参数不同,返回值不同,不构成函数重载。
void func(int a, int b) {// 这是一个函数
}int func(int a) { // 这是一个错误,因为与上面的函数返回值类型不同,不构成重载return a;
}
2.参数个数不同
参数个数不同也是函数重载的一种形式。例如:
void func() {cout << "No parameters" << endl;
}void func(int a) {cout << "One parameter: " << a << endl;
}
调用func()会匹配到第一个函数,而func(10)会匹配到第二个函数。
3.参数类型顺序不同
参数类型的顺序不同也可以构成函数重载。例如:
void mix(int a, double b) {cout << "Int and Double: " << a << ", " << b << endl;
}void mix(double a, int b) {cout << "Double and Int: " << a << ", " << b << endl;
}
调用mix(1, 2.0)会匹配到第一个函数,而mix(2.0, 1)会匹配到第二个函数。
可能发生歧义的情况
在C++中,如果一个函数有默认参数,而另一个函数没有参数且函数名相同,这会导致调用时的歧义。例如:
void func() {cout << "No parameters" << endl;
}void func(int a = 0) {cout << "One parameter with default: " << a << endl;
}
在这种情况下,调用func()时编译器无法确定应该调用哪个函数,因为两个函数都可以接受没有参数的情况。因此,这种重载是不允许的,会导致编译错误。
C语言与C++在函数重载上的区别及C++的支持原理
前置知识
一、GCC与C语言的关系
GCC定义:GCC是一个开源的编译器套件,最初是为C语言设计的,后来扩展支持了多种编程语言。
C语言编译:GCC作为C语言的主要编译器之一,负责将C语言源代码编译成机器码,使其能够在计算机上运行。
编译过程:GCC编译C语言代码的过程包括预处理、编译、汇编和链接等阶段。
二、G++与C++的关系
G++定义:G++是GCC套件中的一个编译器,专门用于编译C++代码。
C++编译:G++能够处理C++的复杂语法和特性,包括类、对象、继承、多态等,将C++源代码编译成机器码。
函数重载的原理讲解
C语言不支持函数重载
- C语言的编译过程相对简单,函数名在编译后直接作为符号使用,没有额外的修饰。
- 由于这种设计,C语言无法区分同名但参数不同的函数,因此不支持函数重载。
- GCC作为C语言的编译器,遵循C语言的语法和规则,因此也不会在编译过程中为C语言提供函数重载的支持。
C++支持函数重载
- 为了实现函数重载,C++采用了“名字修饰”或“名字改编”的技术。
- G++作为C++的编译器,实现了C++的名字修饰规则,为每个重载的函数生成一个独特的内部名字。
- 在编译过程中,G++会根据函数的参数类型、参数个数和参数类型顺序等信息对函数名进行修饰,确保编译器和链接器能够区分开不同的函数。
引用
引用的特性
1.引用必须初始化
#include <iostream>int main() {int a = 10;int &ref; // 错误:引用ref没有被初始化// ref = a; // 如果把初始化放在这里,也是不允许的,必须在声明时初始化// 正确的做法int &refToA = a; // 正确:在声明时初始化引用std::cout << "refToA: " << refToA << std::endl; // 输出:refToA: 10return 0;
}
2.1个变量可以有多个引用
#include <iostream>int main() {int a = 10;int &ref1 = a;int &ref2 = a; // 合法:ref1和ref2都是a的引用ref1 = 20;std::cout << "a: " << a << ", ref1: " << ref1 << ", ref2: " << ref2 << std::endl;// 输出:a: 20, ref1: 20, ref2: 20// 说明ref1和ref2都是指向a的引用return 0;
}
3.当一个引用一旦引用一个实体,就不能引用其他实体。
#include <iostream>int main() {int a = 10;int b = 20;int &ref = a; // ref初始化为a的引用// ref = b; // 错误:这里尝试让ref引用b,这是不允许的return 0;
}
引用的应用场景
1.引用做参数
①做输出型参数
通过引用在函数中交换int变量的数据:
#include <iostream>
using namespace std;void Swap(int& a, int& b)
{int tmp = a;a = b;b = tmp;
}
int main()
{int x = 0, y = 1;Swap(x, y);cout << "x=" << x << " " << "y=" << y << endl;//x=1 y=0return 0;
}
在数据结构中(链表的实现)的应用:
typedef struct ListNode{int val;
struct ListNode* next;
}LTNode,*PLTNode;void ListPushBack(PLTNode& phead,int x)
{//...//phead=newNode;//...
}
C++中的引用作为输出型参数具有以下几个优点:
-
语法更简洁:使用引用可以避免复杂的指针解引用操作,使代码更加清晰易读。
-
类型安全:引用在声明时必须被初始化,并且不能为空(尽管它可以引用一个空指针)。这有助于减少因未初始化指针或空指针解引用而导致的错误。
-
语义更清晰:引用明确表示了“别名”的关系,使得代码更易于理解和维护。
②提高效率
主要体现在针对大对象、深浅拷贝上。
- 大对象:在C++中,当处理大型对象或复杂数据结构(如大型类实例、动态数组、链表、树等)时,使用引用作为输出型参数可以显著提高效率。这是因为引用提供了对原始对象的直接访问,而无需复制整个对象。复制大型对象可能会非常耗时,并且会消耗大量内存,特别是在性能敏感的应用程序中。
- 深浅拷贝:后面章节会补充。
2.引用做返回值
①引用返回减少拷贝,提高效率
传值返回时,函数会创建一个临时变量(即返回值的拷贝),然后将其返回给调用者。这意味着对于大型对象或需要深拷贝的对象,传值返回可能会非常低效;引用返回则直接返回对象的引用(别名),避免了不必要的拷贝。这对于大型对象或需要频繁返回的对象特别有用。
注意:
- 函数返回的是局部变量的引用,那么这是危险的
因为局部变量在函数返回后会被销毁,所以返回的引用将指向一个已经不存在的对象,这会导致未定义行为:
在这里的代码中,打印出的b的值是不确定的:如果getRefWrong函数结束后,栈帧被销毁,但是没有清理栈帧,那么b的结果是正确的;如果getRefWrong函数结束后,栈帧被销毁,清理了栈帧,那么b的结果是随机值。
但是如果继续调用其他函数,我们再打印b就不会出现正确的结果了:
输出结果:
- 函数返回的是静态变量的引用,这是没问题的
②提高效率(针对大对象、深拷贝对象)
对于大型对象或需要深拷贝的对象,引用返回可以显著提高效率,因为它避免了对象的拷贝。
③在修改和获取返回值上也很方便
引用返回允许调用者直接修改返回的对象,而不需要通过额外的指针或引用来传递修改:
以前的做法:
#include<iostream>
#include<cassert>
using namespace std;struct SeqList
{int a[100];size_t size;
};int SLGet(SeqList* ps, int pos)
{assert(pos < 100 && pos >= 0);return ps->a[pos];
}void SLModify(SeqList* ps, int pos, int x)
{assert(pos < 100 && pos >= 0);ps->a[pos] = x;
}int main()
{SeqList s;SLModify(&s, 0, 1);//获取第0个位置的值int ret1 = SLGet(&s, 0);cout << ret1 << endl;// 对第0个位的值+5SLModify(&s, 0, ret1 + 5);ret1 = SLGet(&s, 0);cout << ret1 << endl;return 0;
}
有引用的做法:
#define _CRT_SECURE_NO_EARNINGS 1
#include<iostream>
#include<cassert>
using namespace std;struct SeqList
{int a[100];size_t size;
};int SLGet(SeqList* ps, int pos)
{assert(pos < 100 && pos >= 0);return ps->a[pos];
}void SLModify(SeqList* ps, int pos, int x)
{assert(pos < 100 && pos >= 0);ps->a[pos] = x;
}int& SLAt(SeqList& s, int pos)
{assert(pos < 100 && pos >= 0);return s.a[pos];
}int main()
{SeqList s;SLAt(s, 0) = 1;//获取第0个位置的值cout << SLAt(s, 0) << endl;//对第0个位的值+5SLAt(s, 0) += 5;cout << SLAt(s, 0) << endl;return 0;
}
常引用
前置知识:临时变量具有常性
在C++编程中,临时变量是在表达式计算或函数返回时自动生成的,用以存储短暂的数据结果。这些临时变量具备常性,即它们是不可被修改的。原因在于临时变量往往没有具体的名称,因此无法通过名称来对其进行访问或更改。此外,即便临时变量以某种方式变得可访问(例如,通过引用与其绑定),编译器也会坚守其常性,从而避免对其造成意外的修改。
在数据类型转换或函数返回值的场景中,临时变量尤为常见:
①当我们将一个对象转换为与之兼容但类型不同的新对象时,可能会生成一个临时变量来承载转换后的数据。
这里发生了隐式类型转换,创建了int的临时变量
②当函数返回一个对象时,特别是通过值传递的方式,也可能会产生一个临时变量来保存这个返回值。
func1函数返回x时,会生成一个int的临时变量存储返回值x。
权限不能放大,但是可以平移或者缩小
引用的过程中,权限不能放大,但是权限可以平移或者缩小。
权限不能放大:这一原则确保了常量对象(const对象)的不可变性。换句话说,我们不能从一个常量对象创建一个非常量引用,因为这将破坏常量对象的不可修改性。
权限可以平移或缩小:平移意味着保持原有的常量性;缩小则表示我们主动放弃对原本可变对象的修改权限,选择通过常量引用来访问它。
我们会通过几个例子来帮助理解:
①在const变量中:
#include <iostream>
using namespace std;int main() {const int constVar = 10; // 定义一个常量变量// 错误做法:权限被放大了,尝试从常量变量创建一个非常量引用int& nonConstRef = constVar; // 这行代码会编译错误,因为不能从const int创建int&// 正确做法:权限可以平移,从常量变量创建一个常量引用const int& constRef = constVar;// 通过常量引用访问常量变量的值cout << "constVar: " << constRef << endl; // 输出:constVar: 10// 错误做法:权限被放大了,尝试通过常量引用来修改常量变量的值constRef = 20; // 这行代码会编译错误,因为constRef是一个常量引用,不能修改它所引用的值return 0;
}
②在数据类型转换中:
③在函数返回值中也要注意:
引用和指针的区别
语法角度
语法层面上,引用没有开空间,是对a取别名。而指针开了空间,来存储a的地址。
#include<iostream>
using namespace std;int main()
{int a = 10;//语法层面:不开空间,是对a取别名int& ra = a;ra = 20;//语法层面:开空间,存储a的地址int* pa = &a;*pa = 30;return 0;
}
底层汇编指令的角度
但是从底层汇编指令的角度看,引用是类似指针的方式实现的。
其他不同
(不建议背,建议去理解)
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小。
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
- 引用比指针使用起来相对更安全。
总结
- 基本任何场景都可以用引用传参数。
- 但是要谨慎用引用做返回。出了函数作用域,对象不在了,就不能用引用返回,对象还在就可以用引用返回(静态变量、malloc)。
auto关键字
auto关键字能自动推断出变量的类型,让我们写代码更轻松。
作用
1.类型自动推断:编译器会自动根据初始化表达式来推断变量的类型。
2.简化代码:当类型名很长或者复杂时,用auto可以让代码更简洁。
3.避免类型错误:有时候类型写错了可能不容易发现,用auto就能减少这种错误。
代码例子
1.基本使用
#include<map>
using namespace std;int main()
{int a = 0;int b = a;auto c = a; // 根据右边的表达式自动推导c的类型auto d = 1 + 1.11; // 根据右边的表达式自动推导d的类型cout << typeid(c).name() << endl;//检测变量c的数据类型cout << typeid(d).name() << endl;//检测变量d的数据类型return 0;
}
2.与迭代器一起使用
#include<iostream>
#include<string>
#include<vector>
using namespace std;int main()
{vector<int> v;//类型很长//vector<int>::iterator it = v.begin();//等价于auto it = v.begin();map<string, string> dict;//map<string, string>::iterator dit = dict.begin();等价于下面的语句:auto dit = dict.begin();return 0;
}
3.与范围 for 循环一起使用
#include<iostream>
using namespace std;int main()
{int arr[] = { 1, 2, 3, 4, 5 };for (int i = 0; i < sizeof(arr) / sizeof(int); ++i)arr[i] *= 2;for (int* p = arr; p < arr + sizeof(arr) / sizeof(arr[0]); ++p)cout << *p << " ";cout << endl;// 范围for 语法糖// 依次取数组中数据赋值给x,自动迭代,自动判断结束for (auto x : arr)//等价于 for (int x : arr){cout << x << " ";}cout << endl;// 修改数据// auto关键字让编译器自动推断e的类型,&表示e是对容器中元素的引用。// 这意味着通过e修改的值会反映到原始容器arr中。// 如果只用auto e,则e会是容器元素的一个拷贝,对e的修改不会影响到原始容器中的元素。for (auto& e : arr){e *= 2;}// 分别打印出arr里的数据都*2后的结果for (auto e : arr){cout << e << " ";}cout << endl;return 0;
}
auto关键字不能应用的场景
1.auto不能作为函数的参数
2.auto不能直接用来声明数组
内联函数
内联函数提出的背景
C语言宏函数的优点:不需要建立栈帧,提高效率
- 宏函数在编译时会被直接替换为它们的代码体,因此不涉及栈帧的建立和销毁。
- 宏函数的这种特性使得它们可以比普通函数更快地执行,特别是在频繁调用的小函数或性能敏感的场景中。
宏函数的弊端:容易出错,可读性差,不能调试。
- 容易出错:宏函数的参数如果是表达式,可能在宏展开时产生意外的副作用。
- 可读性差:复杂的宏函数可能使代码难以理解,特别是当宏中包含多个语句或嵌套宏时。
- 不能调试:由于宏函数在预处理阶段被替换,调试时无法看到宏的调用,只能看到展开后的代码。
C++内联函数的提出
为了解决C语言宏函数的不足,同时保留其不需要建立栈帧的优势,C++引入了内联函数(inline函数)。内联函数是一种提示编译器将函数体直接插入到每个调用该函数的地方,而不是像普通函数那样进行调用。
inline int add(int x, int y) {return x + y;
}int main() {int a = 5, b = 10;int result = add(a, b); // 编译器可能会将add函数的体直接插入到这里return 0;
}
内联函数使用时需要注意的点
1.适用于内容短小、被频繁调用的函数。内容多的函数不适合当内联函数,容易出现代码膨胀,增加程序的内存占用。
我们来举个例子:假如这里有一个Func函数,编译好后是有50行的指令,而在一个项目中,有10000个位置调用了Func,如果Func不是内联函数,合计起来有10000+50条指令(10000个call Func调用函数的指令和50条函数自身的指令);如果Func是内联函数,合计起来有10000×50条指令(因为每一次调用都要把内联函数展开)。最终会导致可执行程序变占用的内存变大(安装包变大)。
inline对于编译器来说仅仅只是一个建议,最终是否能成为inline,编译器自己决定。(比如,比较长的函数、递归函数,就算加了inline,也不会成为内联函数。)
2.默认Debug模式下,inline不会成为内联函数,不然会不方便调试。
如果需要让inline在Debug模式下成为内联函数,需要进行下面的设置:
3.inline不建议声明和定义分离,分离会导致链接错误。
在C/C++中,通常建议将函数的声明放在头文件中,而将函数的定义放在源文件中。这样做的好处是可以提高代码的可维护性和可重用性。然而,对于内联函数来说,这种做法可能会导致问题。当内联函数的声明和定义分离时,如果多个源文件都包含了该内联函数的声明,并且都尝试使用该内联函数,那么在链接阶段就可能会遇到链接错误。原因:内联函数没有独立的函数地址,内联函数在编译时会被展开,因此它不会像普通函数那样在生成的二进制文件中占有一个独立的地址。如果某个源文件中的代码尝试获取该内联函数的地址(例如通过函数指针),那么编译器将无法找到这个地址,因为内联函数根本就没有生成独立的函数体。
// F.h
#include <iostream>
using namespace std;
inline void f(int i);// F.cpp
#include "F.h"
void f(int i)
{cout << i << endl;
}// main.cpp
#include "F.h"
int main()
{f(10);return 0;
}// 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl
f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
为了避免上述问题,通常建议将内联函数的声明和定义放在一起,通常是在头文件中。
指针空值nullptr(C++11)
NULL的问题
在C++11之前,空指针(即不指向任何有效内存地址的指针)通常通过NULL宏来表示。然而,这种表示方法存在一些问题,我们通过一个代码例子来看看会有什么问题:
我们发现,f(0)和f(NULL)调用的都是第一个f函数。这是因为NULL通常被定义为0或者((void*)0),这意味着它既可以被解释为整数0,也可以被解释为指针类型。
nullptr的提出
为了解决这些问题,C++11引入了一个新的关键字nullptr,用于表示空指针。nullptr的类型是std::nullptr_t,这是一个特殊的类型,专门用于表示空指针。对于上面的代码,如果我们往f函数传入nullptr,就会调用第二个f函数。