一、第一个C++程序
今天我们正式开始C++语言的学习,和C语言一样,我们与C++的第一缕羁绊从打印 “hello world” 开始:
#include <iostream> using namespace std; int main() { cout << "hello world" << endl; return 0; }
我们知道,C++是对C语言的完善以及再发展,所以C++中的很多东西是与C语言十分修饰的,并且C++也是兼容C的,也就是说,我们在 .cpp 文件中编写、运行 .c 程序,编译器也不会报错;那么类比过来,我们也就不难猜到,上述程序中的 iostream 是头文件,cout 是输出函数,对我们来说,唯一感到不解和陌生就只有 using namespace std; 这条语句,要理解这条语句,我们需要学习C++的命名空间相关知识。
二、命名空间
1、什么是命名空间
我们知道,在C语言中,如果我们的程序中包含某一头文件,那么我们就不能定义与其同名的全局变量,否则编译器就会报错;比如,在下面的程序中,我们包含了 <string.h> 头文件,该头文件中含有 trelen 函数,如果再用 strlen 作为变量名定义变量,就会造成重定义:
#include <stdio.h> #include <string.h> size_t strlen = 10; int main() { printf("%d\n", strlen); }
但是C语言头文件中的库函数是非常多的,我们在编写大型项目的时候就难免可能会定义与库函数同名的变量,从而造成命名冲突;为了解决这个问题,C++引入了命名空间的概念。
命名空间:在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存 在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化, 以避免命名冲突或名字污染,其中定义命名空间的关键字是 namespace。
2、命名空间的定义
定义命名空间很简单,只需要使用 namespace 关键字,后面跟上命名空间的名字,然后后接一对 {} 即可,{} 中即为命名空间的成员。
命名空间有如下特点:
- 命名空间的名称是随意取的;
- 命名空间中可以定义函数/变量/类型;
- 命名空间可以嵌套;
- 同一个工程中允许存在多个相同名称的命名空间,编译器最后会将其合成到同一个命名空间中;
命名空间中定义函数/变量/类型:
namespace N1 { //定义变量 int strlen = 10; //定义类型 typedef struct SLNode { int data; struct SLNode* next; }SLNode; //定义函数 void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } }
命名空间嵌套定义:
namespace N1 { int strlen = 10; //嵌套定义 namespace N2 { typedef struct SLNode { int data; struct SLNode* next; }SLNode; } //嵌套多层 namespace N3 { void Swap(int* p1, int* p2) { int tmp = *p1; *p1 = *p2; *p2 = tmp; } } }
同一个工程中允许存在多个名字相同的命名空间,编译器最后会将其合成到同一个命名空间中:
3、命名空间的使用
命名空间的使用有三种方式:命名空间名称加作用域限定符、使用 using 将命名空间中某个成员引入、使用 using namespace 将命名空间名称引入,其中的作用域限定符为:“::”;我们以下面这个命名空间为例:
namespace N { int a = 0; int b = 1; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; }
命名空间名称加作用域限定符:
#include <iostream> using namespace std; int main() { cout << N::a << endl; cout << N::Add(2, 3) << endl; return 0; }
使用 using 将命名空间中某个成员引入:
#include <iostream> using namespace std; using N::a; using N::Add; int main() { cout << a << endl; cout << Add(2, 3) << endl; return 0; }
使用 using namespace 将命名空间名称引入:
#include <iostream> using namespace std; using namespace N; int main() { cout << a << endl; cout << Add(2, 3) << endl; return 0; }
总的来说,我们想要使用命名空间中的变量,一共有两种方法:一种是使用作用域限定符 ::,另一种是引入命名空间,而引入命名空间又分为部分引入和全部引入。
嵌套定义的命名空间的使用:对于嵌套定义的命名空间,我们逐层使用作用域限定符即可,当然也可以通过逐层引入命名空间的方式使用:
4、注意事项
使用命名空间有几个需要理解和注意的地方:
1、一个命名空间就定义了一个新的作用域,这个域叫做命名空间域,命名空间中的所有内容都局限于该命名空间中;
2、命名空间中定义的变量都是全局变量:如下图,命名空间N中的成员变量a可以在函数test被访问,说明a的作用域是全局,所以a是全局变量;
3、编译器查找变量的规则是:默认现在局部域中查找,如果找不到,再到全局域中去找,如果在全局域中也没找到该变量,就报错;而命名空间的作用是改变编译器查找变量的规则,让编译器先到局部域中查找,如果找不到,就直接到命名空间中去找,再找不到就报错。
三、C++的输入输出
C++的输入输出语句如下:
#include<iostream> // std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中 using namespace std; int main() { int a = 0; cin >> a; //console in cout << a << endl; //console out endline return 0; }
说明:1、使用 cout 标准输出对象(控制台)和 cin 标准输入对象(键盘)时,必须包含 <iostream> 头文件 以及按命名空间使用方法使用 std。
2、 cout 和 cin 是全局的流对象,endl 是特殊的C++符号,表示换行输出,他们都包含在包含 <iostream>头文件中。
3、<<是流插入运算符,>>是流提取运算符。
4、使用C++输入输出更方便,不需要像 printf/scanf 输入输出时那样,需要手动控制格式;C++的输入输出可以自动识别变量类型。
5、实际上 cout 和 cin 分别是 ostream 和 istream 类型的对象,>>和<<也涉及运算符重载等知识, 这些知识我们我们后续才会学习,所以我们这里只是简单学习他们的使用。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间, 规定C++头文件不带.h;旧编译器(vc 6.0)中还支持格式,后续编译器已不支持,因此推荐使用+std的方式。
std命名空间的使用惯例:std是C++标准库的命名空间,如何展开std使用更合理呢?
1、 在日常练习中,建议直接using namespace std即可,因为这样很方便。
2、using namespace std展开后,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对 象/函数,就存在冲突问题;
3、该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现;所以建议在项目开发中像 std::cout 这样使用时指定命名空间 + using std::cout 来展开常用的库对象/类型等方式。
四、缺省参数
1、缺省参数的概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值;在调用该函数时,如果没有指定实 参则采用该形参的缺省值,否则使用指定的实参。
2、缺省参数的分类
缺省参数一共分为两类:全缺省参数和半缺省参数;
全缺省参数:
void Func(int a = 10, int b = 20, int c = 30) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; }
半缺省参数:
void Func(int a, int b = 10, int c = 20) { cout << "a = " << a << endl; cout << "b = " << b << endl; cout << "c = " << c << endl; }
注意事项
- 半缺省参数必须从右往左依次来给出,不能间隔着给;
- 缺省参数不能在函数声明和定义中同时出现,如果既存在函数声明,又存在函数定义,那么缺省参数只能在函数声明处给定;
- 缺省值必须是常量或者全局变量。
五、函数重载
1、函数重载的概念
函数重载是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
#include<iostream> using namespace std; //参数类型不同构成函数重载 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } double Add(double left, double right) { cout << "double Add(double left, double right)" << endl; return left + right; } //参数个数不同构成函数重载 void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; } //参数类型顺序不同构成函数重载 void f(int a, char b) { cout << "f(int a,char b)" << endl; } void f(char b, int a) { cout << "f(char b, int a)" << endl; } int main() { Add(10, 20); Add(10.1, 20.2); f(); f(10); f(10, 'a'); f('a', 10); return 0; }=
2、函数重载的原理 (重要)
我们在学习C语言 程序环境和预处理 的时候知道了一个程序要运行起来,需要经历预处理、编译、汇编、链接四个阶段;其中编译阶段会进行符号汇总,汇编阶段会生成符号表,而链接阶段则会对符号表进行合并与重定位,其中符号表会将每一个变量都关联上一个地址,但这个地址是否有效需要在链接阶段进行符号表的合并与重定位是时才能检查出来。
而对于上述过程中生成符号表这一阶段,C编译器与C++编译器所进行的操作是不同的 – C语言编译器会直接用变函数名作为符号表中的符号,而不会对函数名进行修饰;而C++编译器则是会对函数名进行修饰,用修饰后的名称来构成符号表。
注:由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使 用了g++演示了这个修饰后的名字。
采用C语言编译器编译后结果:
采用C++编译器编译后结果:
经过对比后我们发现:在linux下,采用gcc (C语言编译器) 编译完成后,函数名字的修饰没有发生改变;而采用g++ (C++编译器) 编译完成后,函数名字的修饰发生改变,函数名由 前缀_Z+函数长度 +函数名+类型首字母 组成,即编译器将函数参数类型信息添加到了修改后的名字中。
通过上面这个实例就理解了:C语言没办法支持重载是因为同名函数没办法区分;而C++是通过函数修饰规则来区分,只要参数类型不同,修饰出来的名字就不一样,所以就支持重载。
同样,我们也理解了函数的返回值不同以及同类型参数的顺序不同是不构成重载的,因为C++编译器没办法区分;但其实即使是C++编译器把函数的返回值类型也加入了函数修饰规则,也仅仅是让它在语法层面是构成了重载而已,在实际使用中也是不构成重载的,因为函数传参时并不会传递函数的返回值类型,那么对于返回值不同,其他各方面都相同的函数而言,操作系统就不知道应该将参数传递给哪个函数,即在传递参数时出现了二义性,这时候编译也是会报错的。
注:对C/C++函数调用约定和名字修饰规则感兴趣的同学可以拓展学习一下下面这篇文章,里面有对vs下函数名修饰规则的讲解:C/C++ 函数调用约定 。