C++是在C语言的基础之上,容纳进去了面向对象的编程思想和泛型编程,并增加了许多有用的库,以及一些关键字
本节主要介绍C++对于C语言语法不足的补充,以及C++是如何对C语言设计不合理的地方进行优化的,为后续学习类和对象打下基础
首先我们先看一下C++如何打印"hello world"
下面我们会介绍一下
#include
using namespace std;
这两行代码的作用
首先因为C++是兼容C语言的,所以在C++程序里面是可以运行C语言代码的,如图是在C++文件中运行C语言代码
可是.如果我们定义了一个变量或者函数,跟库中的某一个变量或者函数重命名了(即发生了命名冲突问题)会发生什么状况呢?
下面我们来看一下
我们定义了一个变量rand(注意:在stdlib.h头文件中有一个函数叫做rand)
在还没有包括stdlib.h头文件时,程序是可以正常运行的
因为此时还没有发生命名冲突
那么当我们包括了这个头文件时呢?
报错:rand重定义,以前的定义是函数
所以,对于项目开发而言,我们要定义的变量和函数肯定会特别多
我们就算不跟库中的命名发生冲突,也难免会发生与我们一起开发某个项目的同事的命名冲突的问题
如果发生了命名冲突问题,那么修改名字会非常麻烦,会影响项目开发的进度,这是C语言的一个非常大的缺陷
所以C++引入了命名空间这一语法
namespace 命名空间的名称
{
可以放入int,double,char.......
函数,结构体,类等等所有我们可以定义的类型
}
如图,现在我们把我们定义的rand这个整形变量放入了命名空间wzs中,那么此时就不会跟stdlib.h头文件中的rand函数发生命名冲突了
命名空间就像是一个围墙,把rand这个整型变量"封"起来
防止非法访问
默认情况下编译器并不会去命名空间中进行查找,只有指定时才会到该命名空间里面去找
那么如果我们想要访问命名空间wzs中的rand呢?
想要访问呢?wzs::rand来访问
这个::叫做域作用限定符
同理,wzs命名空间中也可以定义函数,结构体等等…
下面我们来看一下
这里请注意,当我们想要访问wzs命名空间下的结构体SListNode时
需要把命名空间的名称放到struct的后面
struct wzs::SListNode* plist=NULL;
如果我们想要定义的数据太多了,出现了一个命名空间中的命名冲突问题呢?
此时我们可以嵌套命名空间
还可以在wzs1或者wzs2中继续嵌套,就像是套娃一样…但是最好不要嵌套太深,否则麻烦的还是自己
那么对于那种在头文件中声明,在源文件中定义的情况呢?
我们定义了一个栈,放到了wzs命名空间中
这里就先不去全部写完了,毕竟我们这节课介绍的不是栈的代码实现
而是命名空间
请注意,我们将这里的命名空间也命名为wzs会报错吗?
答案是:并不会,因为不同文件下的命名空间会被编译器合并到一起,所以并不会报错
Stack.h
#pragma once
#include
#include
//这里的命名空间也命名为wzs会报错吗?并不会
//不同文件下的同名命名空间会被编译器合并到一起,所以并不会报错
namespace wzs
{
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps);
void StackPush(struct Stack* ps, int num);
}
Stack.cpp
#include "Stack.h"
namespace wzs
{
void StackInit(struct Stack* ps)
{
ps->a = NULL;
ps->capacity = ps->top = 0;
}
void StackPush(struct Stack* ps, int num)
{
if (ps->capacity == ps->top)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
int* tmp = (int*)realloc(ps->a,sizeof(int) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top++] = num;
}
}
TestStack.cpp
#include "Stack.h"
int main()
{
struct wzs::Stack st;
wzs::StackInit(&st);
wzs::StackPush(&st, 1);
wzs::StackPush(&st, 2);
wzs::StackPush(&st, 3);
wzs::StackPush(&st, 4);
wzs::StackPush(&st, 5);
return 0;
}
我们调试一下,程序运行正确
但是总是写命名空间真的好麻烦
所以能不能有一些情况下不用每次都指定命名空间呢?
using namespace wzs;//展开命名空间:默认指定在wzs这个命名空间下使用(把wzs这个墙拆掉)
//请注意:这跟展开头文件完全不同,尽管都叫做展开
那么什么情况下我们可以展开呢?
一般情况下不要展开,除非自己写好做测试,没有给别人用,不存在冲突
所以一开始写的using namespace std;(这里的std是C++官方库定义的命名空间)
的意思是
展开C++官方库定义的命名空间
但是在工程项目中不要展开std,容易发生命名冲突
但是在我们自己日常练习的时候为了方便可以展开
如果我们展开的两个命名空间之间本来就有命名冲突呢?
此时会报错
在加上域作用限定符之后就不会报错了
在工程项目中,我们推荐这样使用
比方说我们现在想要很方便的使用使用C++中的cout这个对象,就可以这样去做
(因为cout,cin,<<(流插入运算符),>>(流提取运算符)
涉及到类和对象的知识,我们目前无法展开详细介绍原理)
(endl(end line)相当于C语言中的换行符"\n")
//在工程当中推荐这种写法
using std::cout;
using std::endl;
using std::cin;
我们不建议直接展开std命名空间
但是每次指定cout和endl,cin的命名空间很不方便,而直接展开就会把std全部暴露出来出现冲突风险
所以我们推荐这种指定展开的写法
所以我们就可以在使用cout,cin,endl的时候不用去指定命名空间了
注意:使用cout,cin,endl必须包含头文件iostream
C++中的iostream头文件类似于C语言中的stdio.h头文件
也就是标准IO头文件
C语言中的printf被cout取代,scanf被cin取代,“\n"被endl取代
一般情况下cout,cin,endl比printf,scanf,”\n"更加方便
不过请注意,有一些情况下它们三个也并不方便,(比方说cout对于浮点数精度的控制方法就不如printf方便)
因为C++是兼容C语言的,所以在这个时候可以使用C语言中的printf,scanf,“\n”
cout这里的c的意思:console(控制台/终端)
在这里我们没有展开cout,endl
发现使用cout和endl必须加上std::,太麻烦了,所以在工程中展开我们推荐那种写法
缺省参数的好处是:可以让函数传参更加灵活,这也是C++设计者设计缺省参数的一个原因
诸如这样的函数,它的所有形参都被赋予了默认值,这种情况情况下叫做全缺省参数
void f(int a=1,int b=2,int c=3);
诸如这样的函数,它的形参有一部分被赋予了默认值,这种情况下叫做半缺省参数
void f(int a,int b=2,int c=3);
注意:缺省值必须从右向左给,不能这样给:
void f(int a=1,int b=2,int c);
因为如果我有这么一个函数调用
f(4,5);
就算是5对应的是c,但是4对应的到底是a还是b呢?
这样会产生歧义,因此C++语法规定缺省值必须从右向左给
缺省参数不能跳着给:
void f(int a=1,int b,int c=3);
f(4,5);
不知道给b的是4还是5
//全缺省参数
void f(int a = 10, int b = 1, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
//半缺省参数
void g(int a, int b = 1, int c = 3)
{
cout << a << " " << b << " " << c << endl;
}
int main()
{
f();
f(1);
f(1, 2);
f(1, 2, 3);
//不过请注意:缺省参数不能跳着给,例如这样
//f(, 2, );//err
//g();//err:函数中调用的参数太少
g(1);
g(1, 2);
g(1, 2, 3);
return 0;
}
可见:缺省参数的好处是:可以让函数传参更加灵活,这也是C++设计者设计缺省参数的一个原因
注意:缺省参数不能在声明和定义中同时存在
为什么?
因为要防止出现这种情况:
Stack.h:
Stack.cpp:
哪怕我不用这个缺省参数,自己传了一个实参,都会报错
也就是怕出现定义和声明中的缺省参数值不同的情况
那么这时缺省参数到底听.h的还是.cpp的呢?会产生歧义,因此C++语法不允许缺省参数在定义和声明中同时出现
那么如果我今天就是想要在一个声明和定义分离的一个程序中设计一个含缺省参数的函数
怎么办?
只能在声明中加上缺省参数,定义中不允许加上缺省参数
因为当调用该函数的源文件包含了头文件之后
1.如果缺省参数是设置在定义中,而不是声明中,那么我在这个调用该函数的源文件中是看不到实际的缺省参数的
2.如果缺省参数是设置在声明中的话,那么是可以看到实际的缺省参数的
下面给大家展示一下:
这是将缺省参数设置在定义中的情况
这是将缺省参数设置在声明中的情况
C++是支持函数重载的,
所谓函数重载就是指:两个函数名称相同,但是参数的个数或者类型或者类型顺序不同,即构成函数重载
注意:如果两个函数只有返回值不同的话,并不会构成函数重载
这里以Add函数为例
int Add(int num1, int num2)
{
return num1 + num2;
}
double Add(double num1, double num2)
{
return num1 + num2;
}
int main()
{
cout << Add(1, 2) << endl;
cout << Add(1.1, 1.2) << endl;
//cout << Add(1, 1.2) << endl;//err
return 0;
}
大家注意到了这一行了吧
cout << Add(1, 1.2) << endl;//err
为什么会报错呢?
首先,在C语言的学习中,我们知道int和double类型是可以发生隐式类型转换的
也就是说在Add(1,1.2)这个函数调用中,有可能会发生两种情况:
(1)(int类型的1)被隐式转换为double类型,去匹配double Add(double num1, double num2)
(2)(double 类型的1.2)被隐式转换为int类型,去匹配int Add(int num1, int num2);
也就是说这个函数调用存在歧义,所以编译器会报错
也就是说哪怕我们去掉其中的一个Add函数,这个函数调用就不会报错了
下面是函数重载的几种错误形式
1.两个函数仅有返回值不同
2.参数仅有名称不同或者仅有参数名称顺序不同
注意:并不是说两个函数构成重载之后对它们的调用就不会产生歧义
下面请大家看一下下面两个函数能不能构成函数重载
void f(int num)
{
cout << "f(int num)" << endl;
}
void f(int num = 1)
{
cout << "f(int num = 1)" << endl;
}
答案是:不构成重载,因为参数的个数,类型,类型顺序均相同
那么在请大家看一下这两个函数会不会构成重载呢?
void f()
{
cout << "f()" << endl;
}
void f(int num = 1)
{
cout << "f(int num = 1)" << endl;
}
答案是:构成重载
但是这意味着这两个函数在调用时就真的没有歧义了吗?
这种情况下是可以的
但是这种情况下就不行了:
因为在调用的时候产生了歧义
这就像是一个经典的问题:
你妈妈和你老婆掉水里,你只能救一个,你救谁?
C++懒得跟你折腾,直接在语法上不允许这样做,直接报错
还有一个问题:函数的参数不同就能构成函数重载,那我返回值不同凭什么就无法构成函数重载呢?
这就要谈一下函数名修饰规则了
在谈这个之前,我们先建立一个Func.h,一个Func.cpp,一个Test.cpp文件
并且复习一下C语言阶段的编译链接的知识
现在我们有Func.h,Func.cpp,Test.cpp这三个文件
其中在Func.cpp文件中定义了两个重载的func函数
我们知道编译分为4个阶段:
1.预处理:
头文件展开
宏替换
去注释
条件编译
Fun.h在Func.cpp和Test.cpp中展开
Func.cpp中同时拥有func函数的声明和定义,Test.cpp中拥有func函数的声明和具体调用
Fun.cpp -> Fun.i
Test.cpp -> Test.i
2.编译:
把.i文件进行语法检查(语法分析,词法分析,语义分析)生成汇编代码(.s文件)
Fun.i -> Fun.s
Test.i -> Test.s
3.汇编阶段:
把汇编代码转换为二进制机器码,生成目标文件(.o文件)
Fun.s -> Fun.o
Test.s -> Test.o
4.链接阶段:
合并.o目标文件,链接一些没有确定的函数的地址,合并段表,符号表的重定位等等
生成可执行程序:
Windows: .exe,
Linux: a.out
而这个函数名修饰规则就是在链接阶段进行的
在预处理阶段结束后
Func.cpp中同时拥有func函数的声明和定义,Test.cpp中拥有func函数的声明和具体调用
所谓声明就是一种承诺,是承诺,就要兑现
而链接阶段就是这个承诺兑现的时候
怎么兑现呢?让我的Test.cpp能够找到func函数的定义,就是兑现
怎么找到呢?
(通过函数声明去找地址)
.o文件中有一个东西叫做符号表,符号表中存储了函数名跟函数地址的一种映射关系
在链接阶段,通过符号表跟一种映射关系找到函数的地址,就能进行函数的调用了
而对于这个符号表来说
在C语言中,符号表中只会建立函数名跟函数地址的映射关系,
也就决定了在C语言中不允许存在同名函数,否则在链接阶段会找到多个函数地址,产生歧义
而对于C++来说
有函数名修饰规则:把函数的参数代入进去对符号表中的映射进行修饰
在Linux下可以尝试看到这个名字
Linux下函数名修饰规则:
_Z4func(_Z是前缀+函数名的字符个数+函数名+参数首字母)
_Z4funcdi(第一个参数类型:double,第二个:int)
_Z4funcid(第一个参数类型:int,第二个:double)
int* :Pi
有兴趣,可以自己在Linux下去试试
下面我们在Linux下给大家看一看
我们在Linux下建立了两个文件:
test.c
test.cpp
先看.c:
cpp:
然后我们用gcc把test.c编译出来,编译成testc
然后用这个命令查看testc的内容
这个 < func>前面的就是它的地址,也就是第一个指令的地址,
而且我们发现这个函数名根本就没有被修饰
下面我们来看一下.cpp文件
先用g++把test.cpp编译成了testcpp
还是用这个命令进行查看
然后我们发现C++编译后的函数名的确被修饰了
修饰成了
_Z4funcid
大家也可以试一下其他的类型,指针的话:
例如int* 类型的是:会被修饰为Pi
下面我们就可以回答这个问题了:
因为在链接阶段
C语言文件的函数地址是完全只通过函数名去进行查找的,如果存在同名函数,会导致找到多个函数地址,让函数调用产生歧义
而C++文件的函数名经过了修饰,那么如果两个函数名相同,但是参数不同就可以进行区分了
所以对于构成重载的函数,只会找到那唯一一个,不会产生歧义
Linux下函数名修饰规则:
_Z4func(_Z是前缀+函数名的字符个数+函数名+参数首字母)
注意:不同编译器下的函数名修饰规则是不同的
那么为什么我刚才不在windows下演示呢?
因为windows下的函数名修饰太过复杂
我们把Func.cpp中的代码注释掉,让它发生链接错误
看到蓝色标注的地方,windows下也是一个类型对应一个符号,不过不像Linux下那么直观
以上就是C++入门1的全部内容,希望能对大家有所帮助