1. C++关键字(C++98)
C++总计63个关键字,C语言32个关键字
2. 命名空间
在C/C++中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进行本地化,以避免命名冲突或名字污染,namespace关键字的出现就是针对这种问题的,我们看下面这段代码:
#include <stdio.h> #include <stdlib.h> int rand = 10; int main() { printf("%d\n", rand); return 0; }
我们在C语言中学过,因为rand是一个函数,所以不能使用rand作为变量名称,因此,这段代码编译会出现以下错误
C语言没办法解决类似这样的命名冲突问题,所以C++提出了namespace来解决
2.1 命名空间定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{}中即为命名空间的成员。
例如:
namespace xiaoxie { int rand = 10; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; }
xiaoxie是命名空间的名字,一般开发中是用项目名字做命名空间名。
命名空间中可以定义变量/函数/类型
命名空间还可以嵌套,如下:
//test.cpp namespace N1 { int a; int b; int Add(int left, int right) { return left + right; } namespace N2 { int c; int d; int Sub(int left, int right) { return left - right; } } }
同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
一个工程中的test.h和上面test.cpp中两个N1会被合并成一个
例如:
// test.h namespace N1 { int Mul(int left, int right) { return left * right; } }
注意:一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中
2.2 命名空间使用
命名空间中成员该如何使用呢?比如:
namespace xiaoxie { int a = 0; int b = 1; int Add(int left, int right) { return left + right; } struct Node { struct Node* next; int val; }; } int main() { printf("%d\n", a); return 0; }
在C语言中我们在调用变量/函数/类型时是直接进行调用,但是在调用C++命名空间中的变量/函数/类型时,有以下3种方式:
1.加命名空间名称及作用域限定符
int main() { printf("%d\n", xiaoxie::a); return 0; }
这种方式在每次调用命名空间中的变量/函数/类型时都要单独展开,是最繁琐的一种,也是最安全的一种方式。
2.使用using将命名空间中某个成员引入
using xiaoxie::b; int main() { printf("%d\n", xiaoxie::a); printf("%d\n", b); return 0; }
这种方式是展开该命名空间中某个变量/函数/类型,在后续调用该变量/函数/类型是可以直接使用而不用再次展开,这种方式是比较推荐的一种。
3.使用using namespace 命名空间名称引入
using namespce xiaoxie; int main() { printf("%d\n", xiaoxie::a); printf("%d\n", b); Add(10, 20); return 0; }
这种方式则是将命名空间中所有变量/函数/类型展开,后续使用该命名空间中的变量/函数/类型时,都可直接调用,但一般在项目中,不推荐这种写法,因为这样不安全。
3. C++输入&输出
我们先看看C++是如何输出hello world的
#include<iostream> using namespace std; int main() { cout<<"Hello world!!!"<<endl; return 0; }
std是C++标准库的命名空间名,C++将标准库的定义实现都放到这个命名空间中
说明:
1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含< iostream >头文件以及按命名空间使用方法使用std。
2.cout和cin是全局的流对象,endl是特殊的C++符号,表示换行输出,他们都包含在包含< iostream >头文件中。
3.<<是流插入运算符,>>是流提取运算符。
4.使用C++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。C++的输入输出可以自动识别变量类型。
5.实际上cout和cin分别是ostream和istream类型的对象,>>和<<也涉及运算符重载等知识,后面我们还会更深入的学习IO流用法及原理。
注意:早期标准库将所有功能在全局域中实现,声明在.h后缀的头文件中,使用时只需包含对应头文件即可,后来将其实现在std命名空间下,为了和C头文件区分,也为了正确使用命名空间,规定C++头文件不带.h;旧编译器(vc 6.0)中还支<iostream.h>格式,后续编译器已不支持,因此推荐使用+std的方式。
#include <iostream> using namespace std; int main() { int a; double b; char c; cin >> a; cin >> b >> c; cout << a << endl; cout << b << " " << c << endl; return 0; }
可以看出C++中的cin和cout可以自动识别变量的类型。关于cout和cin还有很多更复杂的用法,比如控制浮点数输出精度,控制整形输出进制格式等等。
std命名空间的使用惯例:
std是C++标准库的命名空间,如何展开std使用更合理呢?
1.在日常练习中,建议直接using namespace std即可,这样就很方便。
2.using namespace std展开,标准库就全部暴露出来了,如果我们定义跟库重名的类型/对象/函数,就存在冲突问题。该问题在日常练习中很少出现,但是项目开发中代码较多、规模大,就很容易出现。所以建议在项目开发中使用,像std::cout这样使用时指定命名空间 + using std::cout展开常用的库对象/类型等方式。
4. 缺省参数
4.1 缺省参数概念
缺省参数是声明或定义函数时为函数的参数指定一个缺省值。在调用该函数时,如果没有指定实参则采用该形参的缺省值,否则使用指定的实参。例如:
#include <iostream> using namespace std; void Func(int a = 0) { cout << a << endl; } int main() { Func(); Func(10); return 0; }
没有传参时,使用参数的默认值,传参时,使用指定的实参。
4.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.半缺省参数必须从右往左依次来给出,不能间隔着给
2.缺省参数不能在函数声明和定义中同时出现,如果生命与定义位置同时出现,恰巧两个位置提供的值不同,那编译器就无法确定到底该用那个缺省值,一般放在声明中
3.缺省值必须是常量或者全局变量
4.C语言不支持(编译器不支持)
5. 函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
5.1 函数重载概念
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。在我们前面讲到的输入输出流cin和cout也是通过这种方式来实现自定识别类型的。
5.1.1 参数类型不同
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; } int main() { cout << Add(10, 20) << endl; cout << Add(10.1, 20.2) << endl; return 0; }
5.1.2 参数个数不同
void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; } int main() { f(); f(10); return 0; }
5.1.3 参数类型顺序不同
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() { f(10, 'a'); f('a', 10); return 0; }
5.2 C++支持函数重载的原理–名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。
以下是GCC 的扩展编译流程概览
首先是预编译阶段(.i)编译预处理指令,第二步编译(.s)进行语法分析、词法分析、语义分析、符号汇总,第三步汇编(.o)形成符号表,由汇编指令转为二进制指令,最后通过链接器完成链接。
实际项目通常是由多个头文件和多个源文件构成,在最后的链接阶段时,链接器会用哪个名字来找呢,在每个编译器中都有自己的函数名修饰规则, 由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使用了g++演示了这个修饰后的名字我们先看下面的c程序
#include <stdio.h> int add(int a, int b) { return a + b; } int main() { int a = 1; int b = 2; add(a, b); return 0; }
在C程序的汇编过程中,函数调用名称并没有进行改变,我们再看看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; } int main() { cout << Add(10, 20) << endl; cout << Add(10.1, 20.2) << endl; return 0; }
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
6. 引用
6.1 引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
注意:引用类型必须和引用实体是同种类型的
6.2 引用特性
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体
比如下面这段代码注释部分的引用没有初始化,编译时就会发生错误,而下面两种引用则是正确的使用方法
#include<iostream> using namespace std; void TestRef() { int a = 10; // int& ra; int& ra = a; int& rra = a; printf("%p %p %p\n", &a, &ra, &rra); } int main() { TestRef(); return 0; }
6.3 常引用
#include<iostream> using namespace std; void TestConstRef() { const int a = 10; //int& ra = a; // 该语句编译时会出错,a为常量 const int& ra = a; // int& b = 10; // 该语句编译时会出错,10为常量 const int& b = 10; double d = 12.34; //int& rd = d; // 该语句编译时会出错,类型不同 const int& rd = d; cout << a << " " << ra << " " << b << " " << d << " " << rd << endl; printf("%p\n%p\n%p\n%p\n%p\n", &a, &ra, &b, &d, &rd); } int main() { TestConstRef(); return 0; }
6.4 使用场景
1.做参数
#include<iostream> using namespace std; void Swap(int& left, int& right) { int temp = left; left = right; right = temp; } int main() { int a = 1; int b = 2; cout << a << " " << b << endl; Swap(a, b); cout << a << " " << b << endl; return 0; }
2. 做返回值
首先我们要清楚传值返回的本质是生成一个返回对象的拷贝作为函数调用返回值
我们再来看传引用返回,先看下面的代码
#include<iostream> using namespace std; int& Count() { int n = 0; n++; return n; } int main() { int& ret = Count(); printf("%d\n", ret); printf("%d\n", ret); return 0; }
在这里ret的结果是未定义的,如果栈帧结束时,系统会清理栈帧置成随机值,所以第二趟的结果是随机值,第一趟的结果虽然是1,但是使用引用返回的本质是不对的,结果没有保障,所以我们得到一个结论:出了函数作用域,返回对象就销毁了,那么一定不能用引用返回,一定要用传值返回。
如果是改成下述场景,则可以使用引用返回
#include<iostream> using namespace std; int& Count() { static int n = 0; n++; return n; } int main() { int& ret = Count(); printf("%d\n", ret); printf("%d\n", ret); cout << ret << endl; cout << ret << endl; return 0; }
下面是一种引用的常规用法,比如栈结构中也可以用到,这里我用一个简易的顺序表进行演示,代码如下:
#include<iostream> using namespace std; typedef struct list { int arr[10]; int size; }L; int& ret(L& a, int b) { return a.arr[b]; } int main() { L a; a.arr[0] = 0; ret(a, 0)++; cout << a.arr[0] << endl; ret(a, 0) = 10; cout << a.arr[0] << endl; return 0; }
6.5 传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
#include<iostream> #include <time.h> using namespace std; struct A { int a[10000]; }; void TestFunc1(A a){} void TestFunc2(A& a){} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
值和引用的作为返回值类型的性能比较
#include<iostream> #include <time.h> using namespace std; struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main() { TestReturnByRefOrValue(); return 0; }
通过上述代码的比较,我们不难发现传值和指针在作为传参以及返回值类型上效率相差很大。
6.6 引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main() { int a = 10; int& ra = a; cout<<"&a = "<<&a<<endl; cout<<"&ra = "<<&ra<<endl; return 0; }
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。比如下面这段代码
#include<iostream> #include <time.h> using namespace std; int main() { int a = 10; int& ra = a; ra = 20; int* pa = &a; *pa = 20; return 0; }
再看它的汇编实现
引用和指针的不同点:
1.引用概念上定义一个变量的别名,指针存储一个变量地址。
2.引用在定义时必须初始化,指针没有要求
3.引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
4.没有NULL引用,但有NULL指针
5.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
6.引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
7.有多级指针,但是没有多级引用
8.访问实体方式不同,指针需要显式解引用,引用编译器自己处理
9.引用比指针使用起来相对更安全