目录
C++是在C语言的基础上,容纳进去了面向对象的编程思想,并且增加了许多有用的库,以及编程范式等,所以C++兼容C,也就是我们写C语言的代码在C++编译环境下也依然能够编译通过。
注意:C++兼容C语言,本篇文章在前半部分主要还是用C语言的书写习惯来讲解C++中的命名空间,所以在前半部分使用的都还是#include<stdio.h>。
一、C++关键字
C++关键字一共有63个。
在这里暂时不进行逐一解释,在后期博客会对此进行分析,大家可以先去菜鸟教程中进行了解,链接如下:C++ 的关键字(保留字)完整介绍 | 菜鸟教程 (runoob.com)。
二、命名空间
在一方面来讲C++是在C的基础上的完善,所以C++要去解决C语言中出现的一些问题,首先就是命名冲突的问题。
2.1 C语言中的命名冲突
我们来观察下面的两个代码:
代码一:
#include<stdio.h>
int rand = 0;
int main()
{
printf("%d\n", rand);
return 0;
}
代码二:
#include<stdio.h>
#include<stdlib.h>
int rand = 0;
int main()
{
printf("%d\n", rand);
return 0;
}
对于上述的两个代码,代码一可以正常运行,但是代码二无法正常运行,这是为什么呢?
在上述图片中,我们知道代码二运行不通过的原因是由于rand重定义,我们包含了库中的头文件,即 #include<stdlib.h> ,上述头文件中定义了一个函数rand,包含头文件相当于把头文件中的内容在预编译期间拷贝一份过来,相当于在全局域中,在代码二中我们也定义了一个全局变量rand,也在全局域中,这时就会有命名冲突的问题。
2.2 C++中命名空间
命名冲突有两种:
- 和库冲突。
- 和项目组中的其他人冲突。
我们知道不同的域中可以有同名的变量,C++解决了C语言中命名冲突的问题,C++使用namespace-命名空间(关键字)来解决命名冲突的问题,使用namespace定义一个域,用域进行隔离。
2.2.1 命名空间的定义
定义命名空间,需要使用到namespace关键字,后面跟命名空间的名字,然后接一对{}即可,{} 中即为命名空间的成员。
例如上述的代码二,我们就可以定义一个名字是A的命名空间,rand就在A这个命名空间域了,将rand变量和stdlib.h中的rand函数进行区分。
#include<stdio.h>
#include<stdlib.h>
namespace A
{
int rand = 0;
}
int main()
{
printf("%d\n", rand);
return 0;
}
在上述代码中,相当于将全局变量rand定义到了一个名字是A的域中,而#include<stdlib.h>是将stdlib.h中的内容拷贝到当前文件中,其中的rand函数在全局域中,两个rand命名的函数和变量不是处于同一个域,就解决了命名冲突的问题。
2.2.2 命名空间的特性
1.命名空间中可以定义变量/函数/类型。
namespace A
{
int rand = 0;
int Add(int x, int y)
{
return x + y;
}
struct A
{
int a[5];
char c;
};
}
2.命名空间可以嵌套。
当一个命名空间足够大的时候,命名空间内部的变量或者函数也会有命名冲突的问题,这时候就需要命名空间的嵌套了。
namespace A
{
int rand = 0;
int Add(int x, int y)
{
return x + y;
}
struct A
{
int a[5];
char c;
};
namespace B
{
int a = 0;
char c = 'k';
}
}
3.同一个工程中允许存在多个相同名称的命名空间,编译器最后会合成同一个命名空间中。
例如test.h和hll.cpp中都有A这个命名空间,在最后编译运行时,会合成到同一个命名空间中去。
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
2.2.3 命名空间的使用
#include<stdio.h>
namespace A
{
int a = 3;
}
int main()
{
printf("%d\n", a);
return 0;
}
对于上述的代码,编译会不通过,在main函数中有要去打印变量a,但在搜索时并没有找到变量a 的定义,这是由于在对变量a进行搜索时,我们不会主动地去命名空间域中去搜索。
对于命名空间域中的成员,我们不能直接使用,什么时候才去命名空间域搜索 ?
命名空间中成员该怎么使用呢?
1.指定访问命名空间域。
在这里我们使用 ::(域作用限定符), ::加在变量的前面,表示去::左边的域访问。注意:如果左边的域是空白,就代表是全局域。
#include<stdio.h>
namespace A
{
int a = 3;
}
int a = 5;
int main()
{
printf("%d\n", ::a); //::左边是空白,代表去全局域访问
printf("%d\n", A::a); //::左边的域是A,代表去A这个命名空间域访问
return 0;
}
2.将命名空间全部展开。(使用using namespace 命名空间名称 引入)
#include<stdio.h>
namespace A
{
int a = 3;
}
using namespace A;
int main()
{
printf("%d\n", a);
return 0;
}
使用using namespace A;将命名空间域A展开,但是展开就相当于将A中的内容放在全局中,我们使用命名空间的初衷是为了防止和别人或者和库中的内容有冲突,将自己定义的变量等使用作用域限定起来,使用using namespace A;将其展开,可能又会造成命名冲突的问题,所以我们不推荐使用这个方式,但是在我们平时写代码的过程中,命名冲突可能性小,所以使用这个方式。
3.把命名空间中常用的部分展开。
C++的标准库是使用的std这个命名空间,我们想要使用cout(输出)和endl(换行),可以单独将他们展开。
#include<iostream>
using std::cout;
using std::endl;
int main()
{
cout << "hello" << endl;
}
2.2.4 补充知识
当一个文件中有三个同名的变量,分别在局部域,全局域以及命名空间域,那么他们的访问顺序是怎样的?
#include<stdio.h>
#include<stdlib.h>
int a = 0; //全局域
namespace A
{
int a = 3;
}
int main()
{
int a = 1; //局部域
printf("%d\n", a);
return 0;
}
对于不同的域中的三个同名的变量来说,优先去访问局部变量,然后是全局变量,最后如果展开了命名空间域或者指定访问才会去命名空间域。
2.2.4 C++库的命名空间
C++标准库中的函数或者对象都是在命名空间std中定义的,所以我们要使用标准库中的函数或者对象都要用std来限定。
我们会发现在写C++时,我们在包含头文件时不再加.h了,这是因为C++有了命名空间这个定义之后,就把C++标准库中的函数或者对象都用std这个命名空间包起来了,为了和旧的库进行区分,就定义了新的标准,不再使用.h了。
三、C++中的输入&输出
在这里由于没有类和对象的知识,所以简单的了解一下即可,之后会进行讲解。
cout相当于输出函数,cin相当于输入函数,endl相当于换行,注意cout和cin都可以连续一行插入或输出多个变量,会自动识别类型。
#include<iostream> //C++中标准库,输入输出是由iostream库提供的
using std::cout;
using std::endl;
using std::cin;
int main()
{
int a = 0;
int x = 0;
double d = 0;
cout << "hello" << " " << 'd' << ' ' << 's' << a << endl; //endl相当于换行
cin >> x >> d;
cout << "hello" << " "<< x << " " << d << endl;
return 0;
}
四、缺省参数
C++前期是在解决C语言中出现的问题,所以我们又引出了缺省参数这一概念。
4.1 定义
缺省参数,声明函数的时候可以让 最右边的连续若干个 参数有缺省值,在调用函数的时候,如果不写相应位置的参数,则调用的参数就为缺省值。
例如:
#include<iostream> //C++中标准库,输入输出是由iostream库提供的
using std::cout;
using std::endl;
using std::cin;
void Func(int a = 0) //在形参位置上,给一个缺省值
{
cout << a << endl;
}
int main()
{
Func(); //没有传参时,使用参数的默认值
Func(5); //有传参时,使用指定的实参
return 0;
}
4.2 缺省参数的分类
4.2.1 全缺省参数
全缺省参数,即在声明时,给每一个形参一个缺省值。
#include<iostream> //C++中标准库,输入输出是由iostream库提供的
using std::cout;
using std::endl;
using std::cin;
void Func(int a = 0, int b = 5,int c = 8)
{
cout << "a =" << " " << a << endl;
cout << "b =" << " " << b << endl;
cout << "c =" << " " << c << endl;
}
int main()
{
Func();
Func(5);//将5传给a
Func(5,10); //将5传给a,将10传给b
Func(5,10,6); //将5传给a,将10传给b, 将6传给c
return 0;
}
注意在这里传参的时候,只能从左往右依次传参,不能跳跃传参。
//即不能如下:
Func(5, ,10);
4.2.2 半缺省参数
半缺省,在声明函数时,给部分参数缺省值,但是这部分也有要求。
- 半缺省参数必须从右往左来给出,不能间隔着给。(如果从左往右来给出,那么在函数传参时就无法分辨是给哪一个参数传的值)
#include<iostream> //C++中标准库,输入输出是由iostream库提供的
using std::cout;
using std::endl;
using std::cin;
void Func(int a, int b = 5, int c = 8)
{
cout << "a =" << " " << a << endl;
cout << "b =" << " " << b << endl;
cout << "c =" << " " << c << endl;
}
int main()
{
Func(5);//将5传给a
Func(5, 10); //将5传给a,将10传给b
Func(5, 10, 6); //将5传给a,将10传给b, 将6传给c
return 0;
}
2.缺省参数不能在函数声明和定义中同时出现,是在声明中出现。
例如我们在test.h文件中提供Func函数的声明,在Test.cpp文件中提供Func函数的定义:
在上述两个文件中,我们在声明/定义中都出现了缺省参数,此时运行不通过。
为什么不允许声明和定义中都给缺省值?为了避免如果在声明和定义中给的缺省值不一致,那么编译器按照哪一个来执行的问题。
那么是声明给or定义给呢?
当然是声明给。
如果定义给的话会出现以下的问题:
如果定义给,声明不给,那么在编译期间就会出错。
4.3 示例:
在这里我们使用之前写过的栈的初始化来表示:
对于上述栈的初始化,如果开辟空间小的话,要进行扩容,但是扩容会有一定的消耗,但是如果开辟空间大的话,就会造成空间的浪费,我们可以使用缺省参数,当已经知道需要开辟的空间大小时指定capacity的大小,未知时,就使用缺省值。
五、函数重载
5.1 定义
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这 些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型 不同的问题。
对于这样的构成函数重载的两个函数,在进行函数调用时,编译器会自动识别实参的类型来确定调用哪一个函数。
注意:两个同名函数只有返回类型不同,不构成函数重载。
5.2 函数重载示例
5.2.1 参数个数不同
#include<iostream> //C++中标准库,输入输出是由iostream库提供的
#include"test.h"
using std::cout;
using std::endl;
using std::cin;
void f()
{
cout << "f()" << endl;
}
void f(int a)
{
cout << "f(int a)" <<a<< endl;
}
int main()
{
f();
f(5);
return 0;
}
上述两个函数的参数个数不同,构成函数重载。
5.2.1 参数类型不同
#include<iostream> //C++中标准库,输入输出是由iostream库提供的
#include"test.h"
using std::cout;
using std::endl;
using std::cin;
int add(int x, int y)
{
cout << "int add(int x,int y)" << endl;
return x + y;
}
double add(double x, double y)
{
cout << "int add(double x,double y)" << endl;
return x + y;
}
int main()
{
cout << add(1, 2) << endl;
cout << add(1.2, 2.3) << endl;
return 0;
}
上述的两个函数函数名相同,他们的函数形参的类型不同,构成函数重载。
5.2.1 类型顺序不同
#include<iostream> //C++中标准库,输入输出是由iostream库提供的
#include"test.h"
using std::cout;
using std::endl;
using std::cin;
void f(int a,char b)
{
cout << "f(int a,char b)" << a << " " << b <<endl;
}
void f(char b,int a)
{
cout << "f(char b,int a)" << b << " " << a << endl;
}
int main()
{
f(5,'g');
f('g', 5);
return 0;
}
上述两个函数的形参参数类型顺序不同,构成函数重载。
5.3 为什么C++支持函数重载
我们使用下面的例子来具体探讨:
要探索C++为什么支持函数重载,我们要从编译链接过程来看。
在c语言中,生成的符号表中记录的只有函数的名字以及地址,即类似于下方(下方只是粗大致画的,实际中并不是这样):
而在C++的符号表记录中,会记录形参的类型,所以它支持函数重载。
六、引用
在c语言中,在学习指针的时候是一大难点,C++中退推出了引用来更加方便的解决了一部分与指针相关的问题。
6.1 定义
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空 间,它和它引用的变量共用同一块内存空间。
类型& 引用变量名(对象名) = 引用实体;
注意:引用类型必须和引用实体是同一类型的。
下面就是一个引用的例子:
int main()
{
int a = 0;
int& b = a; //b是a的别名,b和a使用的同一块空间
int& c = b; //c是b的别名,b是a的别名,即c是a的别名,c,b和a使用的同一块空间
cout << &a << endl;
cout << &b << endl;
cout << &c << endl;
return 0;
}
6.2 引用特性
6.2.1 引用在定义时必须初始化
int a = 0;
int& b = a; //b是a的别名,b和a使用的同一块空间
int& c = b; //正确
int& d; //是错误的,必须要初始化
6.2.2 一个变量可以有多个引用
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
int& c = a;
}
6.2.3 引用一旦引用了一个实体,再不能引用其他实体
#include<iostream>
using namespace std;
int main()
{
int a = 0;
int& b = a;
int& c = a;
//引用一旦引用了一个实体,再不能引用其他实体
int x = 10;
c = x; //在这里是将x的值赋给c,c依旧是a/b的别名
return 0;
}
引用的使用场景
6.3 引用做参数(输出型参数)
6.3.1 示例
#include<iostream>
using namespace std;
void Swap(int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 10;
int b = 5;
Swap(a, b);
return 0;
}
之前我们用函数实现交换两个数的值,要用指针,在这里我们就可以使用引用了,x相当于a的别名,y相当于b的别名,改变x即改变a。
6.3.2 效率比较
我们使用下面的函数来测试引用与非引用的效率。
#include<iostream>
using namespace std;
#include <time.h>
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;
}
我们可以看出使用引用 ,效率明显提高。
6.4 引用做返回值
下面我们会通过几个代码示例来具体体会引用做返回值的优势与弊端。
6.4.1 引用做返回值的优势
在下面,我们会通过一段代码的传值返回和传引用返回来比较用引用做返回值的优势。
在下面的两段代码中我们使用传值返回:
1. n是局部变量
int count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = count();
return 0;
}
2.n是静态变量
int count()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret = count();
return 0;
}
我们会发现在上面的传值返回中,不论函数中的变量是局部变量还是静态变量,都需要有一个临时变量协助完成返回,如果我们不想生成临时变量怎么办?用引用返回。
6.4.2 性能比较
值和引用作为函数的返回值的性能比较:
#include<iostream>
using namespace std;
#include<time.h>
struct A { int a[1000]; };
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.4.3 引用缺点
在一些时候,我们无法用引用作为返回值,它会导致一些错误。
错误示例:
int& count() //返回的是n的别名
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = count();
return 0;
}
在上述的情况下使用引用作为函数的返回类型,就会出问题,在上述代码中出现了野指针的访问。
在上面的图中,我们可以明显的看到当main函数调用count函数时,n创建,当调用结束,栈帧销毁,n是在count函数中的局部变量,n的空间还给操作系统。
在这里ret的值是不确定的,count函数返回的是n的别名,当count函数调用结束,将n的别名对应空间的值(即n的值)拷贝给ret,如果count函数调用结束,栈帧销毁,没有清理栈帧,那么ret的结果侥幸是正确的,如果count函数调用结束,栈帧销毁,清理栈帧,那么ret的结果是随机值。
总结:
1.基本任何场景都可以使用引用传参。2.谨慎使用引用做返回值,出了函数作用域,变量就不在了,就不能使用引用返回,如果出了函数作用域,变量还在,就可以使用引用做返回类型。
6.5 常引用
常引用是指利用 const 修饰的引用类型。 由于引用本身已经绑定不可解绑,因此所用的 const 引用都是底层 const,即引用对象不能改变 (俗称常引用)。
在引用的过程中,权限不能放大,但是引用过程中,权限可以平移或者缩小。
下面通过几个例子来看常引用。
int a = 0;
int& b = a; //正确,b是a的别名
const int a = 0;
int& b = a; //错误,a是const修饰的,不能改变,而b是int类型的引用,引用不能放大权限
const int c = 0;
int d = c; //正确,把c的值拷贝给d,没有放大权限
int x = 0;
int& y = x; //正确,y是x的别名
const int& z = x; //正确,引用能够缩小权限
int x = 0;
int& y = x;
const int& z = x; //当z这个别名时,x的值不能改变
++x; //当是x这个名字时,x的值可以改变
const int& m = 10; //m是常量10的别名,权限的平移
//当类型不同时,会发生隐式类型转换,在转换时,中间会产生临时变量,产生的临时变量具有常性,
double dd = 1.11;
int ii = dd; //当类型不同时,会发生隐式类型转换
int& isi = dd; //错误
double dd = 1.11;
int ii = dd;
const int& rii = dd; //正确
int func1()
{
static int x = 0;
return x;
}
int main()
{
//int& ret = func1(); //错误,函数返回时也会产生一个临时变量,临时变量具有常性,此处是对权限的扩大
const int& ret = func1(); //正确,权限的平移
return 0;
}
6.6 引用和指针
七、auto关键字
随着程序越来越复杂,程序中用到的类型也越来越复杂,可能出现类型难于拼写等问题,这时C++就提出了auto关键字。
7.1 概念
int main()
{
int a = 0;
int b = a;
auto c = b; //根据右边的表达式自动推导c的类型
auto d = 1 + 1.11; //根据右边的表达式自动推导d的类型
//typeid可以打印类型
cout << typeid(c).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
注意:
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型。因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编 译期会将auto替换为变量实际的类型。
7.2 auto的使用事项
7.2.1 auto与指针和引用结合起来使用
用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加&。
int main()
{
int x = 10;
auto a = &x; //推导出来a是指针
auto* b = a; //指定必须是指针
auto& c = x; //指定是引用
return 0;
}
7.2.2 在同一行定义多个变量
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器就会报错,因为编译器实际上只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 10, b = 2;
//auto ac = 0, d=1.11;//错误
7.3 auto不能推导的场景
7.3.1 auto不能作为函数的参数
// 此处代码编译失败,auto不能作为形参类型,因为编译器无法对a的实际类型进行推导
void TestAuto(auto a)
{}
7.3.2 auto不能直接用来声明数组
void TestAuto()
{
int a[] = { 0,1,2 };
//auto a[] = { 4,3,5 }; //错误
}
八、基于范围的for循环
8.1 概念
int main()
{
int arr[] = { 1,2,3,4,5 };
//c语言中访问数组
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
arr[i] *= 2;
for(int *p=arr; p<arr+ sizeof(arr) / sizeof(arr[0]);p++)
cout<<*p<<endl;
//范围for
//依次取数组中数据赋值给e
//自动迭代,自动判断结束
for (auto e : arr)
{
cout << e << " ";
}
cout << endl;
return 0;
}
如果我们要使用范围for来修改数组中的数据:
int main()
{
//修改数据
//错误,取arr中数据依次赋值给e,相当于依次取arr[0]、arr[1]......拷贝给e,e的改变不会影响数组的内容
for (auto e : arr)
{
e *= 2;
}
//正确,e开始是arr[0]别名,然后是arr[1]别名,e的修改会影响数组
for (auto& e : arr)
{
e *= 2;
}
return 0;
}
注意上述的范围for中,auto,e都可以改变,arr是数组。
8.2 使用条件
for循环迭代的范围必须是确定的。
int main()
{
int arr[] = { 0,1,2,3,4,5 };
int* p = arr;
//错误,因为for的范围不确定
for (auto& e : p)
{
e *= 2;
}
return 0;
}
九、内联函数
9.1 概念
9.2 适用情况
内联函数适用于短小的频繁调用的函数,inline对于编译器仅仅只是一个建议,最终是否成为inline,编译器自己决定。
内联函数不适用于:1. 比较长的函数 2. 递归函数
inline int Add(int x, int y)
{
return (x + y) * 10;
}
int main()
{
for (int i = 0; i < 100; i++)
{
cout << Add(i, i + 1) << endl;
}
return 0;
}
9.3 注意问题
inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
十、指针空值nullptr(C++)
void Testptr()
{
int* p1 = NULL;
}
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f((int*)NULL);
return 0;
}
在上述代码中,我们想要调用void f(int*),但是发现每次调用的都是void f(int),在C++98中,字面常量0既可以是一个整形数字,也可以是无类型的指针(void*)常量,但是编译器默认情况下将其看成是一个整形常量,如果要将其按照指针方式来使用,必须对其进行强转(void*)0。