在学习C++之前,我们要先知道C++和C是向上兼容的,也就是说,在cpp文件中既可以写入C++的代码,也可以写C的代码,在日常编写代码中,经常会出现C和C++混编的情况。
此博客都是在 C 的缺陷的基础之上 整理 C++ 中对其的优化。
C++总计63个关键字,C语言32个关键字:
#include
int num = 10;
int main()
{
printf("%d\n", num);
return 0;
}
这个代码是可以正常通过的,但是如果我们在 前面引入 stdlib.h 这个头文件就不能通过了:
#include
#include
int rand = 10;
int main()
{
printf("%d\n", rand);
return 0;
}
// 编译后后报错:error C2365: “rand”: 重定义;以前的定义是“函数”
这里冲突是跟 strlib.h 中的rand函数命名冲突了,也就是说,C语言不能处理这种情况的下的命名冲突。那么我们以后再写一些大型的项目的时候,我们定义了很多的变量,我们再引用某个头文件,这个文件不管是 官方的 还是 第三方写的,都有可能出现这种问题,那么这样就不太好。
所以在C++ 中提出了 namespace 来解决。
我们可以使用 namespace 这个关键字来 定义一个 域,这个域也被称为 是 命名空间,然后再这个域中放入 重名的变量 或者是 函数名等等 就不会在和其他库中的 名字重定义了。
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}
中即为命名空间的成员:
namespace Myname
{
int rand = 10;
}
int main()
{
printf("%d\n",Myname::rand);
return 0;
}
这个域就会把其中的 成员进行隔离。
在C中,也说过域的概念,比如全局域和局部域,这些都是一个域,也就说在一个域中不能有重命名存在,如果我们在 全局定义一个 变量 num ,在局部 定义一个变量 num ,这样的操作是可行的,而且 如果我们两个num 的值不一样的 ,那么是局部优先访问的:
int num = 10;
int main()
{
int num = 20;
printf("%d\n", num);//20
return 0;
}
如果此时我们想要访问 全局的那个num ,那么可以使用 " :: " 域作用限定符,他表示的是," :: " 在 左边这个域去访问,如下述例子," :: " 的左边是空白,那么代表的就是 在全局去访问:
int num = 10;
int main()
{
int num = 20;
printf("%d\n", num);//20
printf("%d\n", ::num); //10
return 0;
}
那么编译器访问域的优先级是: 局部域 > 全局域 > 展开了的命名空间域 or 指定访问命名空间域
当 局部域 没有 这个变量的时候,编译器会到 全局域去搜索,但是如果前两个都没有,那么默认他是不会到 命名空间里面去搜索的:
例子一:
int num = 10;
namespace Myname
{
int num = 20;
}
int main()
{
int num = 30;
printf("%d\n", num);// 30
return 0;
}
例子二:
int num = 10;
namespace Myname
{
int num = 20;
}
int main()
{
printf("%d\n", num);// 10
return 0;
}
例子三:
namespace Myname
{
int num = 20;
}
int main()
{
printf("%d\n", num);
return 0;
}
上述是默认的不会再命名空间去搜索,但是有两种方式来访问命名空间里面搜索:
除了使用 " :: " 域作用限定符之外,还有使用 (using namespace xxx)的方式来把这个隔离的 命名空间域展开,展开之后就可以使用了:
namespace Myname
{
int num = 20;
}
using namespace Myname;
int main()
{
printf("%d\n", num);//20
return 0;
}
我们在看 C++ 的代码的时候,经常会看到如下图所示的代码:
现在我们就可以理解这串代码的含义了,就是展开 std 这块空间,这块空间是 标准库里的一块命名空间。
理解展开命名空间域:
所谓展开就是,原本我们定义的命名空间域是与其他域是相互隔离的,也就是说我可以在命名空间域中 创建和其他域中具有相同名字的成员,但是如果我展开了命名空间域,那么相当于是把其中的成员暴露在 全局之中,那么如果此时全局中有重命名的成员,就会报错;或者说是展开之后,里面的成员就是在全局域当中的:
那从此来看, 展开命名空间域( using namespace xxx ) 不太好,因为我们定义 命名空间域本质意义就是不想出现重定义,那么我展开之后不就和之前C中一样了吗?所以我们在使用 ( using namespace xxx )的时候要慎重!!!
所以我们在访问命名空间当中的成员的时候,一般使用的方式是 指定访问命名空间域。
由此看来,在C++中使用 命名空间当中的变量,要比 在 C中使用全局变量来使用,要安全得多。
定义的命名空间名字一般是 项目开发的名字作为命名空间的名字,那么既然里面可以是成员,那么变量,函数,结构体等等都是可以遍历的:
namespace Myname
{
int num = 20;
void Add(int a, int b)
{
return a + b;
}
struct ListNode {
int data;
struct ListNode* next;
};
}
当然还可以在命名空间域里面定义命名空间域(命名空间域的嵌套):
namespace nums
{
int nums = 10;
namespace Array
{
int Array[1] = { 0 };
int nums2 = 20;
}
}
嵌套主要为了在命名空间里面去区别重命名,假设要定义一个很大的库,那么这个库中会定义命名空间,那么这个空间中成员会很多,多就容易导致冲突,所以我们在命名空间里面在去定义命名空间来区别外层最大的命名空间中的重命名。
如果我们来访问嵌套中的命名空间中的成员,我们就需要一层一层的去访问:
namespace nums
{
int nums = 10;
namespace ArrayNums
{
int nums = 20;
}
}
int main()
{
printf("%d\n", nums::nums);//10
printf("%d\n", nums::ArrayNums::nums);//20
return 0;
}
访问嵌套宏的命名空间成员的书写格式(假设有N层最外层为N):
N层命名空间名::N-1命名空间名:: ······ ::需要访问的成员所在命名空间的名字::成员名
同一个工程中有多个相同的命名空间,那么编译器会把这些相同名字的命名空间合并到一个命名空间中:
#include"head1.h"
#include"head2.h"
// 自己定义的头文件
//在head1.h头文件中定义bit 命名空间
namespace bit
{
int num1 = 1;
}
//在head2.h头文件中定义bit 命名空间
namespace bit
{
int num2 = 2;
}
namespace bit
{
int num3 = 3;
}
namespace bit
{
int num4 = 4;
}
int main()
{
printf("%d\t", bit::num1);//1
printf("%d\t", bit::num2);//2
printf("%d\t", bit::num3);//3
printf("%d\t", bit::num4);//4
return 0;
}
当我们在不同bit 命名空间 的定义下,创建了相同的成员,就会报错:
我们在包含C++ 的头文件的时候,没有写 .h 这个后缀:
当我们写上 .h 之后就报错了:
其实在很早之前的 C++ 标准是 有 .h 的,在一些很老的编译器,比如 VC6.0 上都是有 .h
头文件的,但是这些都是老的C++ 标准,而且这些头文件之中是没有 命名空间的。
在C++有了命名空间之后,就把C++里 全部的库都挪到 std 命名空间里面去了,都用std 命名空间给包起来了,为了跟老的库进行区分,就定义了新的一套库,相当于是把老的库给抛弃了。然后就做了一个大胆的决定,不需要 .h 了。
当我们想要访问,这些库当中成员的时候,就需要展开命名空间或者是 指定访问命名空间:
我们一般在使用 库中的内容的时候,都是使用指定访问命名空间,这个方式来使用,一般不会把直接std展开。
那么如果使用指定访问命名空间就有个问题,假设代码中有很多行代码没有指定访问命名空间,现在需要去修改,那么如果我一行一行的去改的话,很麻烦。
我们可以展开我们 经常使用的 命名空间中嵌套的命名空间(指定某个):
假设现在有这样一些代码需要我来指定访问命名空间:
如果我手动去输入的话,很麻烦。我可以单独展开 cout 和 endl:
using std::cout; // 展开 cout
using std::endl; // 展开 endl
int main()
{
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
cout << "hello world" << endl;
return 0;
}
这样就可以找到 cout 和 endl了。
但是这样的方法也是不能展开过多的,因为展开过多之后也就相当于是 全部展开了 。
以后再使用命名空间的时候,假设我们在项目的时候,(假设这个项目和库中的重名),把项目的整个实现代码都包在一个命名空间中,然后如果以后我们想要 调用我们自己的 项目,就在之前说明命名空间就行了。
#include
// std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
using namespace std;
int main()
{
cout<<"Hello world!!!"<
上述的 cout 是标准输出流,endl 相当于一个 '\n' ,也就是说上述的打印 hello world还可以这样写:
在C++当中 一般 都使用 endl 来代替 '\n' 。上述代码的意思其实就是,把 “hello world” 字符串流向标准输出流当中,因为 可以自动判断 类型,我们还可以这样输出:
我们发现上述的 x 变量 直接打印在屏幕上,而且把 x 两边有无空格来进行对比,发现输出结果是一样的。
上述的类型的自动判断,使用函数重载实现的。
如果我们想在字符串和 x 变量之间空格来分隔,那么我们应该在其中添加一个 空格的字符串:
使用cin 标准输入流 和 >> 来输入:
当我们一次输入多个变量的值的使用,可以使用 空格 来输入多个变量的值;也可以使用是 回车 来判断 多个变量的输入:
但是我们发现,上述例子:当我们输入的double类型的数,小数过多的时候,精度会丢失:
这个原因是上诉输出输入流当中的 自动判断类型 产生的,当然也可以指定精度,但是在C++中比较麻烦。我们一般使用C当中的printf()格式化输出来实现精度的打印。
问:
为什么 C 当中的 printf()比 C++ 当中的 cout 要快,这是因为,C和C++的IO缓冲区是不同的,而且C++要做到 C的兼容,我们在C中写printf()可以不管不顾的执行,但是C++不能不管不顾,他每一次在cout 的时候,除了在C++ 当中的缓冲区中寻找之外,他还要在C 当中的缓冲区中去寻找,所以printf()要比 cout 要快,当 cout 和 printf()的数很多之后,这个差别就体现出来了。
所谓缺省参数,就是在 函数声明或定义的时候 如果这个函数需要参数,我们可以给这个参数一个默认值,假设我们在外部调用这个函数没有使用传这个参数,那么就使用这个默认值:
int fun(int x = 10)
{
return ++x;
}
int main()
{
int num1 = fun(20);//21
int num2 = fun();//11
cout << num1 << "\n" << num2 << endl;
return 0;
}
当有多个 参数的情况 ,分为全缺省参数 和 半缺省参数:
void Func(int a = 10, int b = 20, int c = 30)
{
cout<<"a = "<
半缺省参数:
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<
那么当有多个 缺省参数的时候,就可以选择性 按顺序 传入或不传入参数:
void Func(int a = 10, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func();
Func(1);
Func(1,2);
Func(1,2,3);
return 0;
}
输出:
上述为什么说是按顺序呢?因为在C++当中是不支持跳过 一个默认参数来 传入下一个参数的值的:
我们现在想跳过中间那参数,直接传入后面一个参数,我们发现这样是不行的,编译器已经报错了。
论原因其实也没有什么原因,就是C++ 的祖师爷觉得这样不行,所以在定义的时候没有选择上述的方式。
传参必须是从左往右来传参。
void Func(int a, int b = 10, int c = 20)
{
cout<<"a = "<
半缺省参数也是有规则的,必须从右往左 缺省。
也就说下述代码是不行的:
这样写编译器直接报错。
比如我们要实现一个链表,顺序表,栈等等数据结构,都是需要初始化的,那么此处我们就可=可以用到 缺省参数,假设我们在初始化的时候,给定一个参数,就是初始化的时候该初始化多大的栈,但是这样就有一个缺陷,就是用户在初始化的时候可能不知道该定义多大的栈,那么我们就给一个默然参数:
typedef struct Stack
{
int* a;
int top;
int capacity;
}Stack;
void StackInit(Stack* prev,int defaultCapacity = 4)
{
prev->a = (int*)malloc(sizeof(int) * defaultCapacity);
if (prev->a == NULL)
{
perror("malloc fail");
return;
}
prev->top = 0;
prev->capacity = defaultCapacity;
}
int main()
{
Stack std;
StackInit(&std);
}
实现上述效果,在C中是使用 #default xxx 来定义给定 初始化的大小,但是这样本质上也是写死的,只能程序员去代码中修改大小,用户不能修改初始化值。
我们上述说到了,缺省参数可以在函数的定义和声明的时候定义,那么是否声明和定义都需要写呢?
答案是不行的,如果我们在 cpp 文件中和 其对应的 头文件中都去 定义默认参数,那么就会报错:
我们只能再声明或者是定义中选择一个带定义 缺省参数。
原因也很简单,就是如果我们一个默认参数,在声明和定义中都定义了,而且声明和定义的默然参数的值不一样,那么编译器不知道该听谁的,也就是不知道该是用哪一个默认值。
但是在选择声明和 定义 来给定缺省参数的时候需要注意的是,因为我们在调用 我们写的 .h 文件和 .cpp 文件的时候,一边是 引用 头文件,我们知道,C和C++的编译器在编译的时候,会把引入的头文件展开,但是如果此时,我们是在 cpp 文件中定义的 缺省参数,头文件中没有定义 缺省参数,那么就会报错,如下所示:
此时我们在text.cpp 中定义了 缺省参数,在 text.h 中没有定义缺省参数,当我们调用这个函数的时候,我们发现报错了。
当这个程序进行编译的时候,会把头文件中的内容展开,如上图所示,那么此时,我们调用的这个函数,在上述声明的时候,没有 定义缺省参数,我们又只传入一个参数,所以他会报错。
在C++中允许在同一个域当中,定义一些功能类似的同名函数,这些同名函数的形参列表中的 参数个数,参数类型,类型顺序,不同,也就说上述的三种只要有一个不同那么就可以定义一个同名的函数。
用这样的方式,来处理同一个函数传入各种类型不相同代表变量。
如下面的这两个Add()函数就构成了函数重载:
int Add(int x, int y)
{
return x + y;
}
double Add(double x, double y)
{
return x + y;
}
下述这个例子就不构成函数的重载,会报错:
而且就算是返回值的类型不一样,像上述的例子也是不能构成函数的重载的:
所以我们在定义函数的重载的时候,说的是形参的类型,个数和形参类型的顺序来判断
但是需要注意,函数重载和 缺省参数一起用的时候可能会出问题:
void func()
{
cout << "func()" << endl;
}
void func(int a = 0)
{
cout << "func(int a = 0" << endl;
}
int main()
{
func(10);// 可以编译通过
func(); // 编译报错
return 0;
}
上述我们给了一个func(int a = 10 )的缺席参数,但是当我们 在 func() 函数中不给值的话,那么编译器不知道我们现在要调用的是 上述定义的那一个 func()函数。
那么编译器会直接报错:
我们知道C是不支持函数重载的,如果我们使用重载是会直接报错的,C++是支持的,那么我们之前也说过,C和C++是在同一编译环境下编译的,那么这个编译器是如何识别 C 的重载和C++ 的重载的呢?
假设我们现在对Stack.h Stack.cpp Test.cpp 这三个文件进行处理:
首先在程序执行的开始,首先进行的是 预处理操作,预处理主要是进行,头文件展开/宏的替换/条件编译/去掉注释。他会先拷贝一份代码,保存在 Stack.i 和 Test.i 这个文件中,然后再在这个文件中进行 头文件的展开等等操作。
然后是编译,编译做了一件听起来简单,但是实际上很麻烦的 检查语法 操作,然后生成汇编代码,也就是说,我们上述定义的两个函数,如果我不在外面去像刚刚一样去使用他们,那么他们在编译时期是不会报错,因为这两个函数在编译时期是没有语法错误的。只要当我们在像上述使用这两个函数的时候,编译器才会找不到使用使用哪一个函数,从而报错。
在生成 汇编代码之后,又会重新生成一个 Stack.s 和 Test.i 文件来保存这个汇编代码。
然后是汇编操作,上述的汇编代码是 指令式的语言, 本质上还是 符号的形式,cpu 看不懂,cpu只看得懂二进制的 操作,那么 这里的汇编操作就是把汇编语言 转换成 二进制机器码。
然后再 把这些二进制机器码 保存到 Stack.o 和 Test.o 文件中。
最后是链接,把 上述生成的文件链接在一起,生成可执行文件,在window下是 后缀为 .exe 的文件;
我们在调用函数的时候,汇编当中是这样做的:
上述的 call 和 jmp 都是跳转的指令,就是说,程序执行的时候,会先把函数的地址储存到一块空间当中,然后当我们在外面调用函数的时候,会先用 call 来跳转到 储存函数地址的 内存中去函数的地址,然后跳到 函数当中去执行代码,如下图,就是存储函数地址的空间的当中的 汇编代码:
也就是说,我们每一次调用函数都要去 call 一下这个函数地址,再比如上述这个例子,因为我们不是在 Test.cpp 这个文件中去实现 这个函数的,而是在其他的 cpp 文件中去实现 这个函数的,那么这个函数的地址就是在这个 cpp 文件中的汇编代码中就有体现。
但是因为我们只是引用了头文件,头文件中只是声明了这个函数,而仅仅是声明,我们在编译阶段是拿不到这个函数的地址的,上述也说过:
我们由 Stack.h Test.cpp 这两个文件生成了 Test.i 这个文件 ,这个 Test.i 文件中是没有 函数的地址的,函数的地址是储存在 Stack.i 当中的。但是到了最后链接生成 可执行文件的时候,使用 对应生成的 .o 文件,把每一个 .o 文件都链接起来,生成的可执行文件。
而每一个 .o 文件中,都会有一个类似符号表的空间,用来存储 函数的 地址:
上述就是简单 说明 C和C++ 在编译时期 的编译过程。
那么根据上面的 编译过程,C找地址 相对来说很好找,因为一个函数就对应一个函数名,只需要根据这个函数名找到这个地址,也就是直接用函数来找。
但是C++ 当中有 如果有 调用重载函数的时候,是如何找到 我们想要调用的那个函数的呢?
来看这个例子,我们在 .c 文件中写入这个例子:
int func()
{
printf("func(int a, double d)\n");
}
int main()
{
func();
return 0;
}
我们在Linux 环境下查看 汇编代码:
我们发现,在汇编代码中,函数名就是我们定义的函数名,地址也是唯一的。
而我们在换到 .cpp 文件中写入 函数重载:
struct Stack
{};
void StackPush(struct Stack* pst, int x)
{}
void StackPush(struct Stack* pst, double x)
{}
int func(int a)
{
printf("func(int a)\n");
}
int func(int a, double d)
{
printf("func(int a, double d)\n");
}
此时我们再在 Linux 环境下 查看 汇编代码:
此时我们发现,重载的函数 func () 在汇编代码中,名字已经变了,这个名字是这样取的:
这里使用了函数名修饰规则,它把参数给带进来了,_z 是前缀,一般都是_z ;而 4 代表的是函数名的长度,上述的 func 长度是4 ;而后面的 i和d 就是类型的缩写。int 缩写是 i ,double 的缩写是 d。
我们再来看 StackPush()这个函数重载的命名规则:
上述的 P5Stack是 前面传入的 Stack* 这个类型的缩写。
所以这也是为什么,我们能根据我们传入的参数的类型不同,从而可以调用不同的重载函数。
那么此处我们又在想:我们把函数的修饰规则给改了,那么岂不是就可以实现比如 再加一个重载条件,return的类型不同也可以构成重载呢?
答案是不行的。
因为假设我们修改了函数命名规则,把return返回值类型也加了进去,然后我们写了两个只有return类型不同的函数,如下所示:
int func()
{}
double func()
{}
int main()
{
func();
return 0;
}
此时,如果我在 main函数中去调用 func函数,我不用传参来 表示我需要调用哪一个函数,return都是函数调用的时候,有了return,编译器才知道return的是什么类型的数据,我们在编译时期就不知道我们要调用哪一个函数,那么我们如何去 知道 return的类型是什么呢?这就犯了前后的逻辑顺序的错误。
上述在C++当中的修改函数名,只是在符号表当中修改了,并不是直接修改函数名。
另外,并不是所有的函数都需要最后去链接找地址:
首先我们搞清楚,什么函数才需要去链接找地址,就是在本文件中没有定义的函数,之前我们使用的函数都是只有声明的,没有定义,而函数的定义时就有了地址的体现,如果我们在本文件中有了函数的定义,那么就可以直接找到地址,不需要再去链接找地址。
C++的祖师爷觉得 C 当中的指针用这不太方便,所以定义一种新的方式来实现这种效果。
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
间,它和它引用的变量共用同一块内存空间。
可以这样理解,以前在C当中的 指针是 开辟新的一块空间开存储 目标地址,相当于是老板请了一个秘书,其他人可以通过秘书来联系上这个老板,同样,C当中还有二级指针,相当于是秘书(一级秘书)也请了一个秘书(二级秘书),让其他人可以通过这个秘书(二级秘书)来联系自己(一级秘书)。
但是C++当中引用不是这样了,他是给这个块空间取了一个别名,别名和变量共用一块空间,相当于是老板有多个电话,通过这些电话你可以联系上老板。
格式:
类型& 引用变量名(对象名) = 引用实体;
int main()
{
int a = 10;
int& b = a; // 此时 b 就是 a 的别名
int& c = a;
int& d = a;
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
cout << &d << endl;
return 0;
}
输出:
我们发现输出的都是 a 的地址,那么我们看到我们使用了 & 符号来定义一个引用类型,在C中 & 表示的是取地址,其实在C++当中 & 也有取地址的意思,像上述 输出 &a 就是取地址,只不过C++不想 更多的定义 符号,有些符号是连用的。像这里的 & 可以创建引用类型,也可以取地址。
我们创建引用类型之后,这个引用类型就和 本来的引用实体共用一块空间,也就是说,有个人叫 李逵 ,我们定义了 一个 铁牛的 引用类型 和一个 黑旋风的 引用类型,那么当我们交李逵过来的时候,铁牛和黑旋风都过来,我们叫 铁牛 李逵和 黑旋风都过来了······
也就是说,此时我们不管对谁进行修改,那么其本身和 所以的引用类型都要改变,因为他们共用一块空间:
我们可以看到,不管对那个进行修改,其本身和 所以的引用类型的值都变了。
当我们在创建b这个引用类型的时候不给其指定引用实体,我们发现 编译器直接报错了。
int main()
{
int a = 10;
int& b = a;
int x = 20;
b = x;
return 0;
}
如果这个例子,我们先让 b 引用类型引用 a ,然后再定义一个 x 变量,那么此时的 b = x 是把x 的值赋值给b呢,还是把 b 的引用对象改为 x?
答案是赋值给b,也就是说,此时的a和b都被改为20,我们输出一下a的值就知道了:
我们发现,a 的 值被修改为 20,说明此时是把x 的值赋值给b,并没有修改 b 的引用对象。我们要记住C++当中的引用是不能被修改的,创建的时候指向的是哪个对象,自始至终都是指向那个对象的。
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 = " << x << " " << "y = " << y << endl;
return 0;
}
输出:
我们发现,实现了交换效果。
此处Swap传入的参数:
相当于是对应位置的参数的引用类型,引用了对象位置的变量。
那么我们此时相对两个指针变量进行交换,也是可以实现的:
void Swap(int*& x, int*& y)
{
int* tmp = x;
x = y;
y = tmp;
}
int main()
{
int x = 0, y = 1;
int* px = &x, * py = &y;
cout << "px = " << px << " " << "py = " << py << endl;
Swap(px, py);
cout << "px = " << px << " " << "py = " << py << endl;
return 0;
}
我们发现,实现了指针的交换。因为指针也是一个变量,我们也可以对它 创建引用类型。
我们之前在C当中,如果想要在函数中对一级指针进行修改,就要使用二级指针,现在就不用那么麻烦了,引用类型就很直观方便。
我们在学校上课的时候,会发现这样的代码:
typedef struct ListNode
{
int val;
struct ListNode* next;
}ListNode, *PLSListNode;
void ListPushBack(struct ListNode*& phead, int x); // 1
void ListPushBack(ListNode*& phead, int x); // 2
void ListPushBack(PLSListNode& phead, int x); // 3
{
·····
phead = NULL;
·····
}
1 2 都是引用的是一级指针,3 其实也是。