文章目录
前言
一、c++关键字
c++一共有63个关键字,C语言32个关键字,也就是说c++在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/c++库中的名称相同而不能使用,有了命名空间后,以前不能在c语言中使用的名称可以在命名空间中使用,要访问只需要在前面加上域作用限定符即可。当然,关键字是不能使用的即使在命名空间也不行
rand以前是随机数的函数,现在被我们定义成了int变量,所以报错人的重定义。想要解决这样的问题就需要命名空间。
从图中我们可以发现,除了关键字不能使用像库中的函数名等都可以在命名空间使用。
当然命名空间也支持嵌套:
如上图所示, 对于要访问命名空间中的命名空间只需要加入合适的域名限定符即可。
在同一个工程中允许存在多个相同名称的命名空间,编译器最后会把他们合成到一个命名空间中。
一个命名空间就定义了一个新的作用域,命名空间中的所有内容都局限于该命名空间中。
当然命名空间不仅仅有上图中一种使用方式,下面是三种方法的对比:
加命名空间名称及作用域限定符。
这就是我们使用的那个方法。
使用using将命名空间中某个成员引入。
使用using namespace命名空间名称引用
三:c++输入和输出
std是c++标准库的命名空间名,c++将标准库的定义实现都放在这个命名空间中。
1.使用cout标准输出对象(控制台)和cin标准输入对象(键盘)时,必须包含<iostream>头文件以及按命名空间使用方法使用std;
2.cout和cin是全局的流对象,endl是特殊的c++符号,表示换行输出,他们都包含在<iostream>头文件中。
3.<< 是流插入运算符, >> 是流提取运算符。
4.使用c++输入输出更方便,不需要像printf/scanf输入输出时那样,需要手动控制格式。c++的输入输出可以自动识别变量类型。
四:缺省参数
缺省参数就是声明或定义函数时为函数的参数指定一个缺省值,如果你在调用这个函数的时候没有传参数,那么这个函数的参数的值就是缺省值,如果你在调用这个函数的时候传了参数,那么这个函数的参数的值就是你所传参数的值。
缺省又分为全缺省和半缺省,全缺省的意思就是函数的每个参数都给了缺省值,半缺省的意思就是函数的参数只给了个别的缺省值。
1.半缺省参数必须从右往左依次来给出不能间隔着给,比如上图49行代码应该从z开始依次向左给出。
2.缺省参数不能在函数声明和定义中同时出现。
3.缺省值必须是常量或者全局变量。
五:函数重载
函数重载是函数的一种特殊情况,c++允许在同一个作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数,类型,类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
第一种: 参数类型不同
void test(int x, int y)
{
cout << "test(int x,int y)" << endl;
}
void test(double x, double y)
{
cout << "test(double x,double y)" << endl;
}
int main()
{
int a = 10, b = 20;
double x = 1.22, y = 3.22;
test(a, b);
test(x, y);
return 0;
}
第二种:参数个数不同
void test(int x)
{
cout << "test(int x)" << endl;
}
void test()
{
cout << "test()" << endl;
}
int main()
{
int a = 10;
test(a);
test();
return 0;
}
第三种:参数类型顺序不同
void test(int x, char y)
{
cout << "test(int x,char y)" << endl;
}
void test(char y, int x)
{
cout << "test(char y,int x)" << endl;
}
int main()
{
int a = 10;
char c = 'a';
test(a,c);
test(c,a);
return 0;
}
以上就是函数重载的演示,不知道大家有没有想过为什么函数返回值不能做函数重载呢?
因为如果返回值可以重载的话那么你在调用函数传参的时候该怎么区分呢?如下图
两个函数都传一个int类型的参数编译器根本无法区分到底调用哪个。
由于windows下函数重载命名的方式太过复杂,我们在linux环境下看g++编译完成后函数名字的修饰是如何改变的。
我们可以发现testii和testdd就是test的两个函数,编译器通过函数参数不同去命名不同的函数。
六:引用
c++中的引用非常好用,可以避免我们在c语言中使用一级指针二级指针等等,引用不是重新定义一个变量,而是给这个变量起一个别名,他和他引用的变量在同一个空间地址。
如图所示k是a的引用,在我们查看a和k的地址的时候发现他们俩的地址相同,说明他们在同一块空间。在这里要说明一点,引用类型一定是和引用实体是同种类型的。
在a自加后k也自加了也能证明k就是a。
以前需要传地址才能交换两个数现在直接用引用就能解决。
引用的特性:
1.引用在定义时必须初始化
2.一个变量可以有多个引用
3.引用一旦引用一个实体,再不能引用其他实体
因为b已经是a的引用了,然后b又去当c的引用所以就报错了。
引用使用都有什么场景呢?第一个场景就是刚刚swap函数中用引用做参数,第二个场景就是用引用做返回值。
用引用做返回值有什么好处呢?我们学过C语言的都知道当函数结束需要返回的时候会创建一个临时变量去接收返回值然后销毁函数栈帧,那么在创建临时变量的过程中无疑会浪费空间,我们发现当一个变量是静态的或者是函数结束不被销毁的,那么我们就可以用引用返回这样就避免了空间的浪费。
#include <assert.h>
#define N 10
typedef struct Array
{
int a[N];
int size;
}AY;
int& PosAt(AY& ay, int i)
{
assert(i < N);
return ay.a[i];
}
int main()
{
Array ay;
for (int i = 0; i < N; i++)
{
PosAt(ay, i) = 10 * i;
}
for (int i = 0; i < N; i++)
{
cout << PosAt(ay, i) << " ";
}
cout << endl;
return 0;
}
从上图中我们可以看到ay是我们创建的结构体对象,这个对象在函数结束才会销毁,那么像以前C语言那样每次函数返回一个值就需要开一个临时变量去接收返回值很浪费空间,既然这个变量在函数调用后没有被销毁那么就可以直接返回自己,所以在上图中我们使用了引用做返回值。
用引用返回有两个特点:
1.减少拷贝 2.调用者可以修改返回对象
int& Add(int a, int b)
{
int c = a + b;
return c;
}
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;
return 0;
}
上面这个代码有什么问题呢?这个代码是正确的吗?很明显这个代码是错误的,我们已经说过引用返回仅限于函数栈帧销毁后还存在的变量,add的返回值c作用域仅限于add函数当add函数返回就被销毁了,那么这个时候c的空间是不能被访问的。
常引用:
如图所示我们发现好像不能去引用const修饰的变量,这是为什么呢?
这是因为指针和引用,在赋值/初始化的时候权限只能缩小,不能放大,本来a的权限仅仅是只读,结果在引用的时候给了aa可读可写的权限,这当然是不可以的。
int main()
{
int c = 1;
int& cc = c;
//权限的放大
//const int a = 10;
//int& aa = a; //权限的放大会报错
// const int*ptr = NULL
// int* pptr = ptr
//权限的缩小
int a = 10;
const int& aa = a; //权限的缩小没有问题
int* p = NULL;
const int* p1 = p;
return 0;
}
上图中为什么会报错呢?我们可以看到count函数的返回类型为传值返回,传值返回返回的是临时变量,由于临时变量具有常性,所以我们必须加上const
int count()
{
int n = 0;
n++;
return n;
}
int main()
{
const int& ret = count();
return 0;
}
上图中的代码怎么修改才是正确的呢?因为从int转换成double需要隐式转换,而类型转换会产生临时变量,还是刚刚的问题临时变量具有常性所以加上const就可以了
int main()
{
int i = 0;
const double& rb = i;
return 0;
}
引用和指针的区别:
在语法概念上引用就是变量的别名,没有独立空间,和其引用实体共用一块空间。
而在底层实现上实际上是有空间的,因为引用是按照指针的方式实现的。
int main()
{
int a = 10;
int& ra = a;
ra = 20;
int* pa = &a;
*pa = 20;
return 0;
}
从上图我们可以看到int& ra = a 和 int* pa = &a 的反汇编实现是一样的,这也可以证明引用是按照指针的方式实现的。
引用和指针的不同点:
1.引用概念上是定义一个变量的别名,指针存储一个变量的地址
2.引用在定义时必须初始化,而指针可以不初始化。
3.引用在初始化引用一个实体后就不能再去引用其他的实体,而指针可以在任何时候指向任何一个同类型实体。
4.没有空引用,但是有NULL指针。
5.在sizeof中的含义不同,引用的大小是其引用实体类型的大小,而指针永远是32位下4字节,64位下8字节
6.引用自加就是其引用实体自加,而指针自加是指针往后偏移一个类型的大小。
7.有多级指针,但是没有多级引用。
8.访问实体的方式不同,指针需要显示解引用,引用由编译器自动处理。
9.引用比指针使用起来相对更安全
七:内联函数
在c++中一般不在用宏了,一般都用const和enum去替代宏常量,用inline去替代宏函数。
那么为什么c++中不使用宏了呢?因为宏的缺点很明显,第一:不能调试。第二:没有类型安全的检查。第三:有些场景下非常复杂。
为什么说非常复杂呢?大家可以现在用宏写一个ADD函数
#define Add(x,y) ((x)+(y))
int main()
{
//如果将宏定义成这样#define Add(x,y) (x)+(y)
int ret = Add(10, 15) * 3; //结果为(10)+(15)*3 与我们想的(10+15)*3就不一样了
int a = 1, b = 3;
//如果将宏定义成#define Add(x,y) x + y
int ad = Add(a & b, a | b); //结果为 a & (b+a) | b,因为+的优先级高于&和|所以会先进行+
return 0;
}
以inline修饰的函数叫内联函数,编译时c++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。看到这里大家也发现了这不就是宏的优点吗,既然宏的优点被替代了那么自然就很少再使用宏了。
下面是内联函数的反汇编:
inline int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(1, 2);
cout << "ret:" << ret << endl;
return 0;
}
下面是普通函数的反汇编:
int Add(int x, int y)
{
return x + y;
}
int main()
{
int ret = Add(1, 2);
cout << "ret:" << ret << endl;
return 0;
}
通过对比我们发现内联函数在汇编中直接展开,不会再像普通函数那样开一个函数栈帧进入这个函数。
内联函数的特性:
1.inline是一种以空间换时间的做法,如果编译器将函数当做内联函数处理,在编译阶段,会用函数体替换函数调用。缺陷:可能会使目标文件变大。优势:少了调用开销,提高程序运行效率。
2.inline对于编译器而言只是一个建议,不同编译器关于inline的实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现),不是递归且频繁调用的函数采用inline修饰,否则编译器会忽视inline特性
3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
八:auto关键字
使用auto关键字可以让编译器自动推导其类型。
int main()
{
int a = 0;
auto b = a;
auto c = &a;
cout << typeid(b).name() << endl;
cout << typeid(c).name() << endl;
return 0;
}
用typeid().name()可以查看auto推导的是什么类型。
auto的实际价值:简化代码,当类型很长的时候,可以考虑自动推导。
在这里就会有人说了,typedef不是也可以起到简化代码的作用吗?可以是可以,但是typedef在一些场景下会有很大的缺点,比如:
typedef char* pstring;
int main()
{
const pstring p1;
const pstring* p2;
return 0;
}
大家可以看一下上面的代码哪条会报错呢?
答案是p1,这就让人很疑惑了,为什么const char* p1会出错呢?出错的原因在于使用typedef重命名char*后,p1实际上变成了char* const p1,const去修饰p1很明显p1变成了一个常量,常量的定义必须初始化。这就是typedef的缺点。
我们在使用auto的时候,可以强制类型,比如:
int main()
{
int a = 10;
auto* aa = &a; //强制aa是指针类型,当然不加*编译器也能自己推导出来aa的类型
//auto aa = &a;
char c = 'a';
auto& d = c; //auto后想要其是另一个变量的引用必须加上引用符号
cout << typeid(aa).name() << endl;
cout << typeid(d).name() << endl;
return 0;
}
注意:使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。因此auto并非是一种类型的声明,而是一个类型声明时的“占位符”,编译器会在编译期会将auto替换为变量实际的类型。
用auto在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。如下图所示:
正确的应该是:
int main()
{
auto a = 10, b = 20;
auto c = 2.33, d = 2.20;
return 0;
}
auto不能推导的场景:
1.auto不能作为函数的参数
2.auto不能直接用来声明数组
九:基于范围的for循环
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误,因此c++11中引入了基于范围的for循环。for循环后的括号由冒号“ : ”分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围,如下图所示:
int main()
{
int array[] = { 1,6,7,4,2,9,5 };
//自动依次取数组中数据赋值给e对象,自动判断结束
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
return 0;
}
如果赋值给数组中的元素直接用e赋值即可
int main()
{
int array[] = { 1,6,7,4,2,9,5 };
for (auto e : array)
{
e *= 2;
cout << e << " ";
}
cout << endl;
for (auto e : array)
{
cout << e << " ";
}
cout << endl;
return 0;
}
通过上图我们可以发现赋值后好像并没有改变原数组,这该怎么办呢?其实很简单,我们定义迭代的变量的使用用引用即可。
范围for的使用条件:
for循环迭代的范围必须是确定的,对于数组而言,就是数组中第一个元素和最后一个元素的范围。
如下图所示,这样的代码就不能使用范围for:
我们在学习C语言的时候就知道,数组传参只是数组首元素地址,是不知道数组有多少个元素的,需要将数组内的元素大小也传过来。
十:指针空值nullptr
c++中的nullptr实际上是打的一个补丁,因为c++中的NULL出了bug,如下图所示:
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
return 0;
}
按照我们所想f(0)应该调用的第一个f函数,f(NULL)调用的应该是传指针的那个函数,但是事实却并不是这样。
我们可以看到都调用了f(int)这个函数,这是因为在C中NULL实际上是一个宏,如下图:
我们可以看到在c++中NULL被定义为0.在c++11中打了一个补丁加了一个关键字nullptr,nullptr是能正确使用的。
void f(int)
{
cout << "f(int)" << endl;
}
void f(int*)
{
cout << "f(int*)" << endl;
}
int main()
{
f(0);
f(NULL);
f(nullptr);
return 0;
}
如上图所示,nullptr正确匹配了f函数。
注意:
1.在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是c++11作为新关键字引入的。
2.在c++11中,sizeof(nullptr)与sizeof((void*)0)所占的字节数相同。
3.为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。
总结
本次所讲解的都是从c过渡到c++所改进的一些东西,这些东西更偏向于语法,需要大家动手去练习才能更好地记住。