前言
C++在C语言的基础上扩展而来,兼容C语言,解决了C语言的一些弊端与缺陷,并且容纳了面向对象编程思想,拥有许多强大实用的库,以及编程范式
今天我们先来了解学习C++是如何对C语言的一些设计缺陷进行优化的
命名空间
命名空间旨在解决命名冲突的问题,包括程序员与库、程序员与程序员之间的命名冲突
程序员与库的命名冲突是指:程序员创建的变量或者函数与库中的变量或者函数发生命名冲突
而程序员与程序员的命名冲突就是两个程序员的命名发生冲突,通常发生在项目合作中
首先来看下面一段代码
#include <stdio.h>
int rand = 1;
int main()
{
printf("%d", rand);
return 0;
}
rand是全局变量,在局部函数中输出rand,没有问题,下面对代码稍作修改,包含头文件stdlib.h
#include <stdio.h>
#include <stdlib.h>
int rand = 1;
int main()
{
printf("%d", rand);
return 0;
}
运行发生了报错:
rand发生重定义,我们知道 ,程序预处理阶段头文件将会展开,而头文件 stdlib.h
中包含函数 rand()
,那么 rand()
就与全局变量 rand
发生命名冲突,导致重定义错误
在解决上面的问题之前,我们先来看一些前置知识
编译器在进行搜索时,会遵循以下原则:
- 搜索局部域
- 搜索全局域
- 指定一个域,如果指定了,优先去搜索指定域,然后是局部,全局
接着看以下代码:
#include <stdio.h>
int x = 1;
int main()
{
int x = 0;
printf("%d\n", x);
return 0;
}
一个全局变量 x
,一个局部变量 x
,在打印时会优先搜索局部域,打印结果为 0
那如果我们想打印全局变量 x 呢?那就需要域作用限定符 ::
了
::
的左边接作用域,如果空着什么都不写的话,默认是全局域。::
的右边接作用域的变量或者函数等
如果想打印 全局变量x
就接域作用限定符就可以了,::x
#include <stdio.h>
int x = 1;
int main()
{
int x = 0;
printf("局部变量x:%d\n", x);
printf("全局变量x:%d\n", ::x);
return 0;
}
下面我们来看命名空间 namespace
定义命名空间,需要使用到
namespace
关键字,后面跟命名空间的名字,然后接一对{}
即可,{}
中即为命名空间的成员。成员可以是变量、函数和结构体等
命名空间限定了一个域,命名空间中的成员只作用于该命名空间
既然命名空间是一个域,那么命名空间也就可以配合域作用限定符使用
直接上代码
#include <stdio.h>
namespace ns1
{
int x = 1;
}
namespace ns2
{
int x = 2;
}
int main()
{
printf("ns1-x:%d\n", ns1::x);
printf("ns2-x:%d\n", ns2::x);
return 0;
}
命名空间 ns1 中定义了变量 x,命名空间 ns2 中也定义了变量 x,两个变量 x 虽然同名,但并不会发生冲突,因为它们的作用域是自己的命名空间
那如果命名空间重名了会怎样呢?编译器会将两个命名空间进行合并,如果两个重名的命名空间中有重名变量,那这两个变量就会冲突
命名空间重名,变量重名:
#include <stdio.h>
namespace ns1
{
int x = 1;
}
namespace ns1
{
int x = 2;
}
int main()
{
printf("ns1-x:%d\n", ns1::x);
printf("ns2-x:%d\n", ns1::x);
return 0;
}
发生冲突:
发生以上情况,要么给命名空间改名,不进行合并;要么将命名空间中的变量改名,防止命名冲突
了解到以上知识后,我们就可以解决最初的问题
#include <stdio.h>
#include <stdlib.h>
int rand = 1;
int main()
{
printf("%d", rand);
return 0;
}
只需将变量rand放入命名空间中,在打印时使用域作用限定符
#include <stdio.h>
#include <stdlib.h>
namespace ns1
{
int rand = 1;
}
int main()
{
printf("%d\n", ns1::rand);
printf("%p\n", rand); // rand为函数名,是一个地址
return 0;
}
命名空间内成员的使用
现有命名空间如下:
namespace N
{
int a = 10;
char b = 'b';
}
想使用此命名空间中的成员有三种方式:
-
加命名空间和域作用限定符
namespace N { int a = 10; char b = 'b'; } int main() { printf("%d\n", N::a); printf("%c\n", N::b); return 0; }
这样可以精确访问命名空间内的成员,但是如果要大量使用某个成员,都要加命名空间和域作用限定符,就会有点繁琐
-
使用
using
将命名空间内的某个成员引入#include <stdio.h> namespace N { int a = 10; char b = 'b'; } using N::a; // 将命名空间中的 a 引入 int main() { printf("%d\n", a); // 这里使用 using 引入了a,直接就能使用 printf("%c\n", N::b); // b没有使用 using 引用,所以使用时需要引用 return 0; }
-
使用
using namespace
将整个命名空间引入
#include <stdio.h>
namespace N
{
int a = 10;
char b = 'b';
}
using namespace N; // 将整个命名空间 N 引入
int main()
{
printf("%d\n", a); // 命名空间已经引入,其中的成员都可以直接使用
printf("%c\n", b);
return 0;
}
这里不要把引入命名空间和包含头文件搞混了。
文件中包含的头文件会在预处理阶段展开,就是将头文件代码拷贝到文件中
而引入命名空间是让编译器到指定命名空间进行搜索;不引入命名空间,编译器只会搜索局部域和全局域,不会去搜索特定命名空间
当我们使用 using namespace 命名空间
展开时,如果代码较多,容易发生冲突的问题,因此这种方式只推荐在日常练习代码时使用,在编写大型项目时,建议使用前两种引用方式
C++的输入和输出
C++兼容C语言的输入输出方式,也有自己特有的输入输出
首先要引入头文件和命名空间
#include <iostream>
using namespace std;
std
是C++标准库的命名空间,里面存放着标准库的定义的实现
C++的头文件不带.h
,一些旧的编译器(vc6.0)还支持 <iostream.h>
输出
使用 cout
标准输出对象(控制台)和流插入运算符 <<
来进行输出
#include <iostream>
using namespace std;
int main()
{
cout << "Hello World!" << endl; // endl是特殊C++符号,表示换行输出
return 0;
}
可以形象地记忆为 将要输出的内容顺着箭头插入到控制台
输入
使用 cin
标准输入对象(键盘)和流提取运算符 >>
进行输入
#include <iostream>
using namespace std;
int main()
{
int a = 0;
cin >> a ;
cout << "a:" << a << endl;
return 0;
}
也可以像上面那样记忆,将键盘的内容顺着箭头放到目的地
不难看出,使用cout
和cin
,与使用printf
不同,不用我们指定格式,它们会自动识别变量类型
std命名空间的使用
std标准库的内容很多,日常使用时直接展开即可
如果进行项目编写时,不建议这样做,容易发生冲突
建议 std::cin
或者using std::cin
#include <iostream>
using std::cin; // 只引入cin
int main()
{
int a = 0;
cin >> a ;
std::cout << "a:" << a << std::endl; // 引入 count 和 endl
return 0;
}
缺省参数
缺省参数是在声明或定义函数时为函数的参数指定一个缺省值。在调用这个函数时,如果没有指定实参,那么形参就是指定的缺省值;否则使用指定的实参
void Func(int a = 0) // 缺省值为0
{
cout << a << endl;
}
int main()
{
Func(); // 不传实参,形参为缺省值
Func(10); // 传实参,形参为实参的拷贝
}
缺省参数的分类
全缺省参数
全缺省参数是指函数的参数全部缺省,调用函数时只要在函数参数限定个数之内,就可以传任意个参数
比如函数有3个参数,全部是缺省参数,那么调用此函数时可以传0、1、2、3个参数
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func(1, 2, 3);
Func(1, 2);
Func(1);
Func();
}
半缺省参数
半缺省参数不是说函数的参数有一半是缺省,一半不是
半缺省参数是指函数的参数有一部分是缺省,一部分不是
void Func(int a, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
Func(1, 2, 3);
Func(1, 2);
Func(1);
}
需要注意的是,半缺省参数必须从右向左给出,而且必须连续,不能间隔给
如果从左向右给出缺省
void Func(int a = 10, int b = 20, int c);
像上面这种,如果全部传实参还好,但是如果传的参数不足3个,最后一个参数(非缺省参数)就会少参数
间隔给缺省
void Func(int a, int b = 20, int c, int d = 40)
缺省参数不支持间隔跳着给参数,以上这种情况,必须得给 c 传参,那么就必须给a、b传参,但是这样 b 就是去作为半缺省的意义了
缺省参数的应用
当某个函数的某个参数不确定需要什么值,而我们又需要给出一个默认值时,就需要用到缺省参数
比如说顺序栈的初始化
typedef struct Stack
{
int* data;
int size;
int capacity;
}Stack;
void StackInit(Stack* ps, int capacity = 4)
{
ps->data = (int*)malloc(sizeof(int) * capacity);
ps->capacity = capacity;
}
当我们明确知道栈要存多少数据时,就可以给capacity
传参,如果不知道要存多少数据,就给缺省值
注意事项
-
半缺省参数必须从右往左给出,且必须连续
-
如果函数声明与定义分开了,那么缺省参数要放在函数声明中,缺省不能同时存在与声明与定义中
关于这点,我们来详细看一下
函数声明与定义分离,一般是模块化编程时才发生
下面模拟编写栈时的情境
Stack.h 这里存放的是函数声明
#include <iostream> using namespace std; typedef struct Stack { int* data; int size; int capacity; }Stack; void StackInit(Stack* ps, int capacity = 4); // 缺省参数放在声明中
Stack.cpp 这里存放的是函数定义
#include "Stack.h" void StackInit(Stack* ps, int capacity) // 没有缺省 { ps->data = (int*)malloc(sizeof(int) * capacity); ps->capacity = capacity; }
test.cpp 这里用于测试函数
#include "Stack.h" int main() { Stack st; StackInit(&st); return 0; }
我们先来验证缺省是否能同时存在于函数声明与定义中
// Stack.h 函数声明 void StackInit(Stack* ps, int capacity = 4); // 包含缺省 // Stack.cpp 函数定义 void StackInit(Stack* ps, int capacity = 4) // 包含缺省 { ps->data = (int*)malloc(sizeof(int) * capacity); ps->capacity = capacity; } // test.cpp 测试 int main() { Stack st; StackInit(&st); return 0; }
运行程序,发生报错
这时我们再去掉声明中的缺省,保留定义中的缺省
// Stack.h 函数声明 void StackInit(Stack* ps, int capacity); // 去掉缺省 // Stack.cpp 函数定义 void StackInit(Stack* ps, int capacity = 4) // 保留缺省 { ps->data = (int*)malloc(sizeof(int) * capacity); ps->capacity = capacity; } // test.cpp 测试 int main() { Stack st; StackInit(&st); return 0; }
试运行,依然报错
可以看到,错误发生在
test.cpp
中,虽然在函数定义中有缺省,但在函数声明中是没有缺省的,所以发生了如上错误这次我们使用正确方式运行,缺省放在函数声明中,函数定义中不放缺省
// Stack.h 函数声明 void StackInit(Stack* ps, int capacity = 4); // 缺省 // Stack.cpp 函数定义 void StackInit(Stack* ps, int capacity) // 不缺省 { ps->data = (int*)malloc(sizeof(int) * capacity); ps->capacity = capacity; } // test.cpp 测试 int main() { Stack st; StackInit(&st); return 0; }
运行正常,没有报错
那为什么缺省参数要放在函数声明中呢?
首先我们要知道,在程序的预处理阶段,会发生头文件展开、宏替换、条件编译、去注释等操作
在上面的代码中,
test.cpp
包含了头文件Stack.h
// Stack.h #include <iostream> using namespace std; typedef struct Stack { int* data; int size; int capacity; }Stack; void StackInit(Stack* ps, int capacity = 4); // 缺省参数放在声明中 // test.cpp #include "Stack.h" int main() { Stack st; StackInit(&st); return 0; }
在预处理阶段,头文件展开,就是将头文件的内容拷贝到
test.cpp
文件中// test.cpp // ... 拷贝头文件的内容,上面的代码我就省略了,只展示函数声明 void StackInit(Stack* ps, int capacity = 4); // 缺省参数放在声明中 int main() { Stack st; StackInit(&st); return 0; }
预处理结束后,生成一个后缀为
.i
的新文件,内容类似上面的代码,之后会进入编译阶段。在编译阶段,会检查语法是否正确。如上文件中有函数的声明,函数声明中含有缺省参数,所以在调用时只传一个参数不存在语法错误
如果在调用函数时使用缺省参数,而函数声明中没有缺省参数,定义中有缺省参数,在编译阶段会检测出语法错误,导致程序报错
-
缺省参数必须是常量或者全局变量
-
C语言不支持(编译器不支持)
粗略地了解一下程序的翻译链接过程
过程:预处理、编译、汇编、链接
还是模拟编写栈的过程
Stack.h
#include <iostream>
using namespace std;
typedef struct Stack
{
int* data;
int size;
int capacity;
}Stack;
void StackInit(Stack* ps, int capacity = 4);
Stack.cpp
#include "Stack.h"
void StackInit(Stack* ps, int capacity)
{
ps->data = (int*)malloc(sizeof(int) * capacity);
ps->capacity = capacity;
}
test.cpp
#include "Stack.h"
int main()
{
Stack st;
StackInit(&st);
return 0;
}
预处理阶段要做的事情:展开头文件、宏替换、条件编译、去掉注释,最终生成的文件以.i
结尾
编译阶段:检查语法是否正确、生成汇编代码,生成的文件以.s
结尾
在编译阶段,代码转为汇编代码,此时我们就可以了解到函数调用的本质形式:call 函数地址
函数地址又是什么意思呢?这一点和数组有点像
数组由一堆元素组成,数组的地址是首元素的地址
函数由一堆指令组成,函数地址就是函数第一条指令的地址
调用函数实质就是跳转到函数的地址,依次向下执行函数中的指令
在vs中进入调试模式,再转到反汇编:
进入反汇编,我们就可以看到函数调用的汇编形式了,call 函数地址
当文件中没有函数定义,只有函数声明时,暂时是找不到函数的地址的,要到链接阶段寻找函数地址
也就是说,Stack.cpp
中有函数的地址,test.cpp
中没有函数的地址,函数调用暂时记为call StackInit(?)
汇编阶段,将汇编代码转为二进制机器码,生成的文件以.o
结尾
链接阶段:因为只有函数声明的文件找不到函数地址,所以在此阶段,就要凭借函数名去到其他文件中寻找函数地址。最后将各个文件合并到一起,形成可执行程序
函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
不同的问题。
我个人理解,函数重载相当于一词多义,一个函数可以有不同用法
函数重载要求:函数名相同,参数不同,即可构成函数重载
-
参数类型不同
// 整数相加 int Add(int a, int b) { cout << "int Add(int a, int b)" << endl; return a + b; } // 浮点数相加 double Add(double a, double b) { cout << "double Add(double a, double b)" << endl; return a + b; } int main() { Add(1, 2); Add(1.1, 2.2); }
-
参数数量不同
// 零参数 void f() { cout << "f()" << endl; } // 1参数 void f(int a) { cout << "f(int a)" << endl; } int main() { f(); f(1); }
-
参数顺序不同(本质还是类型不同)
//参数顺序不同
void func(int a, char b)
{
cout << "func(int a, char b)" << endl;
}
void func(char a, int b)
{
cout << "func(char a, int b)" << endl;
}
C++函数重载原理
为什么C语言不支持函数重载,而C++支持呢?
首先我们要知道,函数重载虽然函数名相同,但是函数地址不同
int Add(int a, int b);
double Add(double a, double b);
// 以上两函数的地址不同
之前我们已经了解过,在程序的翻译链接阶段,C语言是通过函数名来寻找函数地址的:call 函数名(函数地址)
,C语言无法区分相同的函数名,那就找不到相应的函数地址,自然不支持函数重载
而C++支持函数重载,要寻找函数地址,就要区分相同的函数名。那C++是如何区分的呢?——名字修饰(name Mangling)
具体怎么修饰,不同编译器的修饰规则不同。下面我们以Linux
下的g++
为例,了解一下修饰规则是怎么样的。为什么不用Windows
的vs
?相对来说太复杂了,不如 g++
通俗易懂
g++
名字修饰规则:_Z+函数名长度+函数名+参数类型首字母
int Add(int a, int b); // call _Z3Addii
double Add(double a, double b); // call _Z3Adddd
下面我们就到Linux下进行验证
C语言编译器编译结果
现有test.c
文件,内容如下
编译test.c
文件,形成可执行文件testc
gcc -o testc test.c
反编译testc
objdump -S testc
可以看到,在Linux
下,使用gcc
完成编译后,函数名字的修饰没有发生改变
C++编译器编译结果
现有C++文件test.cc
,文件内容不变
使用g++
编译test.cc
,得到testcpp
g++ -o testcpp test.cc
接着进行反编译
objdump -S testcpp
可以看到,在Linux
下使用g++
完成编译后,函数名字的修饰发生改变,编译器将函数的参数类型信息添加到修改后的名字中