目录
一、前言
1、什么是C++
2、C++关键字(C++98)
二、第一个C++程序
三、命名空间
1、存在意义
2、命名空间定义
3、命名空间的使用
3.1、指定命名空间访问
3.2、全局展开访问
3.3、部分展开访问
四、C++输入&输出
五、缺省参数
1、缺省参数概念
2、缺省参数分类
2.1、全缺省参数
2.2、半缺省参数
3、缺省参数的实际使用
4、总结
六、函数重载
1、函数重载的概念
2、C++支持函数重载的原理
一、前言
1、什么是C++
C语言是结构化和模块化的语言,适合处理较小规模的程序。对于复杂的问题,规模较大的程序,需要高度的抽象和建模时,C语言则不合适。为了解决软件危机, 20世纪80年代, 计算机界提出了 OOP(object oriented programming:面向对象)思想 ,支持面向对象的程序设计语言应运而生。
1982年,Bjarne Stroustrup博士在C语言的基础上引入并扩充了 面向对象 的概念,发明了一种新的程序语言。为了表达该语言与C语言的渊源关系,命名为C++。因此:C++是基于C语言而产生的,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行面向对象的程序设计。
2、C++关键字(C++98)
C++总计63个关键字,C语言32个关键字。
ps:下面我们只是看一下C++有多少关键字,不对关键字进行具体的讲解。后面我们学到以后再
细讲:
asm | do | if | return | try | continue |
auto | double | inline | short | typedef | for |
bool | dynamic_cast | int | signed | typeid | public |
break | else | long | sizeof | typename | throw |
case | enum | mutable | static | union | wchar_t |
catch | explicit | namespace | static_cast | unsigned | default |
char | export | new | struct | using | friend |
class | extern | operator | switch | virtual | register |
const | false | private | template | void | true |
const_cast | float | protected | this | volatile | while |
delete | goto | reinterpret_cast |
二、第一个C++程序
不管是学习什么语言,我们学习道路的起点总是打印一个 "hello world" 。所以我们就先从使用 C++ 打印 "hello world" 来开始本章内容的学习吧。
因为 C++ 是兼容 C语言 的,所以我们打印内容时依然可以使用库函数 printf 。不过一般我们在写 C++ 时,更喜欢使用 cout << "hello world" << endl :
#include <iostream>
using namespace std;
int main()
{
cout << "hello world" << endl;
return 0;
}
运行代码,hello world 就被打印出来了。
这段程序包含了两点知识:
一个是 io流 相关的,比如 "<<" 符号是什么意思。另一个是 命名空间 相关的,比如 namespace 是什么意思。
下面我们就以此为展开,开始本章内容的学习。
三、命名空间
1、存在意义
最开始时,C++出现是为了解决C语言的一些不足。就比如,在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突, namespace 关键字的出现就是针对这种问题的。
举例说明一下:
我们这样写代码,程序正常执行,但是如果我们增加一个头文件 stdlib.h ,再来看一下结果:
可以发现程序直接报错,提示 rand 被重定义了。这是因为我们自己定义的名称与头文件 stdlib .h 里面的名称重复冲突了。
这就会引起一些我们不愿意看到的结果,试想一下,以后我们在项目组里与同事共同协作完成一个项目时,每个人分工写不同模块的代码,是完全有可能使用相同名称的变量、函数名等等的。那这种情况下怎么办呢?让他们全部返工修改吗,这样显然是很不合理的。所以我们需要使用 namespace 来定义命名空间。
2、命名空间定义
在项目开发中,员工A写了一个队列,员工B写了一个链表,他们使用了相同的结构体名称Node:
他们在各自的程序里都没有什么问题,但是当把互相的代码结合到一起时,就会发生命名冲突的报错:
解决方法很简单:我们在程序中增加一个命名空间 namespace ,命名空间定义的是一个域,名为命名空间域。让员工A和员工B把程序放到各自的命名空间域里面去,就不会再发生冲突了。
有别于局部域和全局域,局部域和全局域影响变量的使用范围和生命周期,而命名空间域只影响变量的使用范围,不影响生命周期。
这时我们再运行合并程序,程序就不会再报错了
解决了一个问题之后,我们又遇到了一个新的问题,那就是我们在使用这两个头文件中的结构体类型时,编译不通过。
显示这个结构体变量未定义,可是我们已经在上面两个头文件里都定义过结构体 Node 了,为什么还提示未定义呢?
上面我们说过,命名空间域不会影响生命周期,但是会影响使用范围。我们在调用各种类型、变量等等东西的时候,默认先在局部域寻找,再去全局域寻找,如果都没找到,程序就会报错。也就是说程序不会主动到命名空间域里寻找。
因此,我们定义了命名空间后,需要程序员主动去使用,而不是靠系统程序自动寻找。
3、命名空间的使用
我们使用命名空间有三种方式
3.1、指定命名空间访问
我们使用 "::" 符号来访问命名空间,该符号称为作用域限定符,命名空间写在作用域限定符之前,类型、变量名称写在作用域限定符之后。
这样一来编译就成功了。
需要注意的是, "::" 是写在 Node 前面的,而没有写在 struct Node 前面,这时因为 struct 是结构体类型的前缀,真正同名冲突的是 Node 。
如果 "::" 前不加命名空间,则表明命名空间为全局域。
3.2、全局展开访问
为了防止命名冲突,我们会把变量、类型包到命名空间里面去。使用的时候再借助作用域限定符取出,但是这样使用会有一个问题。
那就是当我们需要频繁使用命名空间里的类型时,就需要不断的写入命名空间与作用域限定符,太过于繁琐,因此我们有了第二种方法:使用 using 将命名空间中某个成员引入。
相当于把命名空间 RDA 在程序里展开了,大家可以理解为程序在寻找类型时,不仅会在局部域和全局域里寻找,还会在命名空间 RDA 里寻找。
所以在一般情况下,我们不建议使用全局展开,因为使用了全局展开之后,我们定义命名空间的意义就没有了。我们平时进行C++代码练习时,为了方便,可以使用全局展开。
3.3、部分展开访问
那么为了能够解决 指定命名空间访问 的繁琐的问题,又要尽量避免使用 全局展开访问。我们来学习第三种方法:使用 using namespace 命名空间名称引入。
我们先修改一下代码:
我们想要多次、频繁的使用 cout 和 endl ,又不想每一次使用都这么麻烦,但是如果直接把命名空间 std 全局展开又太过了。
这时我们就可以把常用的变量、类型、函数等部分展开:
这样就可以在程序中直接使用了。
四、C++输入&输出
接下来我们来讲解C++中,输入与输出的操作:
我们使用 cout << "hello world" << endl;实现了在屏幕上打印 "hello world" 的功能。
其中运算符 "<<" 为流插入运算符,也叫流向。enl 为换行,等价于 "\n" 。
cout 是一个 ostream 类型的对象,这个大家暂时不必理解,后面的文章会详细讲解,暂时把他看作控制台就可以了。
同样还有一个运算符 ">>" 为流提取运算符,可以搭配 cin 使用。 cin 是一个 istream 类型的对象,大家同样暂时可以当作控制台看待。
cout 与 cin 分别为 输出 与 输入,他们与C语言有一个不同的地方,那就是可以自动识别类型。举个例子:
#include <iostream>
using namespace std;
int main()
{
int n = 0;
cin >> n; //输入数字赋值给 n
double* a = (double*)malloc(sizeof(double) * n);
if (a == NULL)
{
perror("malloc fail");
exit(-1);
}
//输入数字赋值给数组 a
for (int i = 0; i < n; ++i)
{
cin >> a[i];
}
for (int i = 0; i < n; ++i)
{
cout << a[i] << endl;
}
return 0;
}
我们先使用 cin 给 int 型变量 n 赋值,再给 double 型数组 a 赋值,并使用 cout 把数组打印出来:
可以看到我们在输入输出的时候并没有指定类型,都是 cin 与 cout 自动识别的,利用这个特性,我们以后的操作会非常的方便。
如果我们想控制输出精度,使用 cout 来控制过于复杂,建议直接使用 printf 函数来操作。
总结:
- 使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
- cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含<iostream >头文件中。
- <<是流插入运算符,>>是流提取运算符。
- 使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
- 实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,这些知识我们后面再来学习,所以我们这里只是简单学习他们的使用。后面我们会更深入的学习IO流用法及原理。
五、缺省参数
1、缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实
参则采用该形参的缺省值,否则使用指定的实参。
例如:
#include <iostream>
using namespace std;
void Func(int a = 0)
{
cout << a << endl;
}
int main()
{
Func(1);
Func();
return 0;
}
当我们主动指定参数给函数时,函数就使用我们指定的参数,否则就使用缺省值。
缺省参数不能在 函数声明 和 定义 中同时出现,这时为了避免出现函数声明和定义中缺省参数给的不同而出现的错误。
如果我们同时写了 函数声明 和 定义,在给缺省参数时,要在函数声明中给。
2、缺省参数分类
2.1、全缺省参数
在声明或定义函数时,我们可以设置多个缺省参数,并且按照从右向左的顺序优先缺省:
#include <iostream>
using namespace std;
void Func(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
cout << endl;
}
int main()
{
Func(1, 2, 3);
Func(1, 2);
Func(1);
Func();
return 0;
}
使用缺省值时,必须要从右往左连续使用。
2.2、半缺省参数
必须从右往左连续缺省,不能间隔着给。
如果函数使用半缺省参数,那么该函数在调用时,一定要指定参数。有几个参数不缺省,就至少要指定几个参数。
3、缺省参数的实际使用
在了解了缺省参数的概念以及使用方法之后,我们来看一下缺省参数实际使用时的场景。
在之前我们学习栈的时候,我们知道栈是通过数组实现的。在栈的初始化时,需要给数组设定一个初始容量大小,这个大小我们之前是设置的 4 。
#include <stdio.h>
#include <corecrt_malloc.h>
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps)
{
ps->a = (int*)malloc(sizeof(int) * 4);
ps->top = 0;
ps->capacity = 4;
}
int main()
{
struct Stack st1;//要存 100 个数
StackInit(&st1);
struct Stack st2;//不知道要存多少个数
StackInit(&st2);
return 0;
}
这种写法其实是不好的。假设有这样一个场景:我们需要两个栈,一个栈要存放 100 个数据,另一个栈不知道要存放多少个数据。如果这两个栈我们都初始化成了 4 ,就很不合理,因为我们都已经知道自己要存 100 个数据了,再让这个栈一次又一次的扩容,会存在资源的浪费。但是如果我们直接就把栈的容量初始化成 100 ,那另一个不知道要存放多少数据的栈又可能用不了这么多,存在空间的浪费。
所以最好的方法就是使用缺省参数:
#include <stdio.h>
#include <corecrt_malloc.h>
struct Stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct Stack* ps, int defaultCapacity = 4)
{
ps->a = (int*)malloc(sizeof(int) * defaultCapacity);
ps->top = 0;
ps->capacity = 4;
}
int main()
{
struct Stack st1;
StackInit(&st1, 100); //直接指定参数 100
struct Stack st2;
StackInit(&st2); //不指定参数
return 0;
}
4、总结
- 半缺省参数必须从右往左依次来给出,不能间隔着给。
- 缺省参数不能在 函数声明 和 定义 中同时出现。
- 缺省值必须是 常量 或者 全局变量。
- 仅支持C++,不支持C语言(编译器不支持)。
六、函数重载
在C语言中,是不允许多个函数名相同的。这样会导致我们在使用几个功能相类似的函数时,要取不同的名字,并且在使用时也要分开使用。
1、函数重载的概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
不同的问题。
例如:
#include <iostream>
using namespace std;
int Add(int num1, int num2)
{
cout << "int Add(int num1, num2)" << endl;
return num1 + num2;
}
double Add(double num1, double num2)
{
cout << "Add(double num1, double num2)" << endl;
return num1 + num2;
}
int main()
{
Add(1, 2);
Add(1.1, 2.2);
return 0;
}
可以看到,C++支持使用形参列表不同的同名函数,并且可以根据参数类型的不同自动识别调用的是哪一个函数。
2、C++支持函数重载的原理
既然C++支持函数重载,那在性能上会不会因为要判断到底是使用哪一个函数而有所下降呢?其实是不会的,因为函数的类型不是在程序运行的时候才开始匹配的,而是在编译链接时就已经匹配完成了。
这里就体现出了C++与C语言的区别,C++在编译时,对函数名进行了修饰。由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,我们就以 g++ 对函数名修饰为例:
在 Linux 中,g++对函数 int Add(int num1, int num2) 作了修饰之后,函数名变为了 _Z3Addii ,_Z是固定前缀,3 是原本函数名(Add)中字符的个数,最后的 ii 是函数中参数类型(int,int)的首字母。同理,函数 double Add(double num1, double num2) 做了修饰之后,函数名变为 _Z3Adddd 。
g++的函数修饰后,函数名变成【_Z+函数长度+函数名+类型首字母】。
我们来实际操作一下,加深印象:
先使用C语言来操作,我们编写如下代码:
使用 gcc 编译之后,输入 objdump -S 指令转到反汇编:
可以看到编译后的函数名就是我们在源程序中写入的 fun ,没有发生变化。
现在我们使用C++来操作:
还是同样的代码,这次我们使用 g++ 来编译,转到反汇编:
可以看到函数名已经被修饰,且修饰规则为【_Z+函数长度+函数名+类型首字母】。
通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
如果C++中两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
下面我们再来看一下 Windows 系统下vs的命名规则,因为比较复杂,我们仅作了解:
对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的,我们就不做细致的研究了。
以上就是C++入门第一部分的内容,主要介绍了命名空间、C++输入与输出、缺省参数以及函数重载等方面的知识,希望同学们多多支持,如果有错误的地方希望大佬指正,谢谢!