目录
c++相对于c语言,弥补了很多c语言上不足的地方,相对来说更加的智能。
c++的语言特点:相比于C语言面向过程,关注过程。C++基于面向对象,关注的是对象但是C++不是纯面向对象的,可以面向对象和面向过程混编。兼容c语言。
基于c++的面向对象,有三大特性。封装,继承和多态。
封装:本质上是一种管理。将数据和操作数据的方法进行有机的结合,隐藏(private和protect的成员函数)对象(成员变量)的属性和实现细节,仅提供对外公开接口(public类型的成员函数)进行交互。
c++中,封装主要是通过类的使用来实现的。
在了解c++的类之前,我们先了解一些前置的概念,以便对c++进行初步的了解和后续c++的学习。
一 标准域和命名空间
首先,我们先写一个最基本的打印“hello world”的程序。通过观察,与c语言相比,我们很容易就能发现, 多了using namespace std;这一行。
#include<iostream>
using namespace std;
int main()
{
cout << "hello world!" << endl;
}
那么这一行在整个程序运行中代表了什么意思呢?为什么要加上这一行呢?
主要是为了防止命名冲突。
那么有人又有疑问了:什么是命名冲突?命名空间又是怎么防止命名冲突的呢?
首先引入一个域的概念:
域:全局域 局部域 命名空间域
平时使用的时候,一般去局部和全局域中查找对应的定义。找不到对应的定义的时候回去命名空间域里面寻找。
那么可以这样解释:
命名空间可以理解成起到了一个隔离的作用。
比如我要去调用一个cout的函数,这是我在栈上开辟的一块空间。但是如果没有命名空间的话,那么就会和C++中的内容重叠,编译器无法识别。这就叫做命名冲突。所以c++中就引出了一个标准库的概念。把标准库的东西放在std里面,这样我后续如果有与本身已经定义的名称起到冲突的自定义函数的话,也会被隔离,因为它们两者处于不同的作用域。
这其实是对c语言的一个优化。c语言中是没有标准命名空间域这一概念的。
举个例子
上述例子中,虽然最后执行程序的时候可以打印0,但是会报错。因为寻找相应变量,编译器默认先去局部域寻找,再去全局域寻找,命名空间域里面寻找,上述去全局域寻找。找到了就可以打印相应的值。但是因为自己定义的printf与标准库中printf冲突了,编译器就报错了。
在实际使用中,命名空间域也有相应的价值。比如不同的人需要编写同一个项目,可能命名有区别,使用命名空间域就可以避免了。
那么命名空间域怎么使用呢?
有人在main函数外部使用标准命名空间。此时的话,就相当于在main函数外部对命名域中的变量和函数进行展开。这种方法其实不太推荐,因为这样和全局变量的定义的方法差不多相同。并没有解决掉名字冲突的问题。
这里给出两种解决的方法:在函数内部指定命名空间或者在函数外部展开常用的命名空间域。
方法一:指定命名空间:
#include<iostream>
int main()
{
std::cout << "hello world!" << std::endl;
return 0;
}
方法二:在类外展开常用的
指定命名空间域对于调用频率比较高的标准库中的函数就很不方便,因此对于这一类的函数可以在类外进行展开。
#include<iostream>
using std::cout;
int main()
{
cout << "hello world!" << std::endl;
return 0;
}
总结一下:使用命名空间域一般采用上述两种方法。对于调用频率较高的函数采用第二种方法。
我们也可以自己定义命名空间域。即自定义命名空间域。
那我们怎么定义呢?
命名空间域中可以定义函数,变量,或者嵌套定义另外的命名空间域。如果相同的命名空间域在不同的文件中,最终编译器也会帮忙合成一个命名空间域。这也解决了不同的人写同一个项目时的命名问题。
namespace num
{
int n1;
int n2;
//定义变量
int add(int num1, int num2)
{
return num1 + num2;
}
//定义hanshu
namespace count
{
int a;
int b;
}
//嵌套定义
}
二 输入输出
对c语言的改进:由于需要进行类型的指定,因此c语言比较麻烦。因此c++不需要指定,可以自动匹配类型。由cin输入,cout输出。
关于底层实现:cin cout其实是istream 和ostream的对象,它们通过运算符重载来实现对不同类型的匹配
关于运算符:
①>>流提取运算符
②<<流插入运算符
其实很形象得描述了控制台与变量之间数据的交互。
>>控制台的数据流向对应的变量。起到了从控制台输入数据的功能
<<将变量中的数据流向控制台。起到了一个数据输出到控制台的作用
③Endl ‘\n’ 换行符
注意:使用流运算符必须包含头文件和命名空间。
对于c++头文件的说明:C++的头文件为了和c相区分,以及对命名空间更好地使用,因此就不带.h
三 缺省参数
函数的声明和定义的时候,可以默认给函数缺省值,这样的参数叫做缺省参数。举个形象的例子,就像现实生活中的备胎只有当函数没有传入参数的时候,才会自动带上之前制定的缺省值。
#include<iostream>
using std::cout;
void test(int a = 0)
{
cout << a << std::endl;
}
int main()
{
test();
return 0;
}
我们可以根据参数缺省值的个数对类型进行划分:全缺省和半缺省参数(部分缺省)
#include<iostream>
using std::cout;
void test1(int a = 0,int b=1,int c=2)
{
cout << "a="<< a << std::endl;
cout << "b=" << b << std::endl;
cout << "c=" << c << std::endl;
}//全缺省
void test2(int a , int b = 1, int c = 2)
{
cout << "a=" << a << std::endl;
cout << "b=" << b << std::endl;
cout << "c=" << c << std::endl;
}//半缺省
int main()
{
test1();
test1(100);
test1(100, 200);
test1(100, 200, 300);
return 0;
}
当半缺省参数的时候,怎么传参?
从左往右给。
那么缺省参数有什么规则?
从右往左连续缺省
因为传参是从左往右进行传参的。所以参数只有从右往左缺省,才有实际意义。
使用场景
struct stack
{
int* a;
int top;
int capacity;
};
void StackInit(struct stack* ps, int capacity=4)
{
ps->a = (int*)malloc(sizeof(int) * ps->capacity);
//因为要只需要演示缺省参数,所以省略判空
ps->top = 0;
ps->capacity = capacity;
}
以这个为例
1 首次初始化的时候,如果一个数据都没有因为是两倍size进行扩容,对于0来说扩容无意义,因此需要对capacity设置缺省值
2 频繁地扩容是有代价的 因此如果实际使用中,我知道自己需要多大的容量,此时设置默认值,就避免了反复扩容带来的速度上的损耗。Malloc在别的位置上牌皮空间,拷贝原先的数据,之后摧毁原来的空间
注意:缺省参数不能在声明和定义的时候同时出现。否则编译器会认为你重定义了。因为担心声明和定义的时候不统一,所以语法规定不可以。
如何写?
如果生命和定义分开的话,默认在声明的时候写出。
但是也可以放在一起。
四 函数重载
c语言中即使实现一个相同的功能,有时候因为传递的参数的类型不同,必须定义多个不同名字的函数。但是c++中,只要形参列表不同就可以了。
要求函数重载必须在同一作用域中。比如都在全局域。需要注意的是在不同的命名空间中,是不能构成重载的。
对顺序不同可以这样理解
void test(int n.char ch)
{
;
}
void test(char ch.int n)
{
;
}
即必须是不同类型的参数顺序不同。如果是相同类型的参数的话,他们没有价值。因为编译器无法去确定到底调用哪个函数。归根结底,调用时就是要让编译器区分。
但是需要注意的是,返回值类型不同并不能够构成重载
为什么c++支持函数重载,而c语言不支持?是如何支持的?
先简单了解一下编译链接的过程。
先编写Func.h func.c main.c这几个文件。fun.h包含相关头文件和函数的声明。fun.c是函数的实现.main.c来调用fun.c
1 预处理:头文件展开 宏替换 条件编译 去掉注释
生成文件类型:.h->.i
2 编译:语法检查 生成汇编代码
生成文件类型:..i->.s
程序最终是由cpu执行的。因为不认识汇编代码,那么需要继续翻译
3 汇编 把汇编代码转换成二进制的机器码
生成文件类型:..o
4 链接 将所有的程序合在一起,生成可执行程序
生成文件类型:..out .exe
为什么c语言不能重载?主要是函数链接出了问题。
一个函数主要包括生命和定义。
函数声明告诉编译器存在这个函数,后续需要编译器去找到对应的函数的定义,再将所有的文件链接在一起,才能生成可执行程序。
函数定义如何取到?func.o里面才有。借助符号表保存对应函数和变量的地址。
这时候就体现了链接的工作。把.o的目标文件合并到一起,其次还需要找一些只给了函数声明的变量和地址的地址。
如果实际找不到就会产生链接错误。C语言是借助函数名字直接生成符号表里的东西。两个func虽然不同,但是无法区分。所以在main函数中找不到
对于c语言
虽然参数列表不同,但是都是会生成func 会产生冲突。
如果是用c++来编译,就可以运行了。
函数调用会转换成对应的汇编指令。
main函数要去调用对应的func(int,int) func(double,int)
由于已经声明了,所以可以call这两个函数,但是没有定义,所以没有函数的地址。所以要去寻找对应的地址。
调用函数的时候call这个函数。func1(06F14C4h)后面的这个其实是是函数指针,是地址,实际上存的是jmp的地址。有了jmp的地址,可以找到对应的函数的地址,从而找到对应函数。
这个调用函数的过程也可以理解成
Call->jmp->对应函数的地址->执行函数 :建立栈帧
c++有一套新的函数名修饰规则
c++根据c++的函数名修饰规则,在符号表中生成了不一样的能够区分的两个函数名以及地址,后续链接找到对应的函数。
所以c语言中如果要确保链接不出问题,就要保证符号表中生成的对应函数名字能够被区分,所以名字不能相同。
五 引用
取别名。给已有的变量取别名。因此他们共用一块内存空间。编译器就不会为取别名地变量来额外地开辟一块内存空间了。要与取地址区分
int main()
{
int a = 0;
int& b = a;//取别名
cout << &b << endl;//取地址
return 0;
}
引用有什么要求呢
1 引用在定义时候必须初始化。既然是取别名就要明确是为谁取得别名呀。
2 一个变量可以有多个引用。就像一个人可以有多个外号,本质上都是这个变量。
3 引用一旦指定了一个实体,就不能再指定其他的实体了。因为无法区分是别名还是赋值。因此如果引用一个实体,接下来对别名进行的操作,本质上是赋值
关于权限放大和缩小
const int a = 10;
int& b = a;//权限的缩小 无法编译
const int& b = a;//权限的平移 可以正常编译
对于c本身来说,不能被修改。但是此时取别名的时候,却可读可写了。权限被放大了。会报错。
只能写成Const int&d=c权限的平移。
权限不能放大,但是权限可以平移和缩小,上述是对同一类型的可读可写权限不同来取别名
那么对于不同类型呢?
int i = 1;
double d = 1;
double& d = i;//无法通过
const double& d = i;//可以通过
类型不同赋值的时候,会产生临时变量。比如ii赋值给dd。会产生一个对ii的拷贝的double类型的临时变量,此时已经进行了转换。
需要注意的是,临时变量具有常性。
也就是说,不同类型之间取别名的话,由于是对一个相当于被const修饰了的临时变量来赋值的,所以别名也应该具有常属性,需要被const修饰。
类比整形提升。
因此当操作符两边的类型不一样的时候,会发生整形提升呀什么的,因此会产生一个临时变量。也就是说,所有的转换不会对元变量进行修改。1 原变量空间可能不够。2 如果其他地方需要对原变量进行访问,那么就无法访问。因此设计时候不会改变原变量的类型,中间会产生一个临时变量。
对于常量
const int& n = 10;
总结:
引用具有很强的适应性,所以使用引用的时候,最好带上const不改变相应的输入参数
传值传参就不涉及这一方面的问题了。
引用的使用场景
做参数
1 做输出型参数。输出型参数是什么?通过一个函数实现,输出的值才是需要得到的值。比如swap。
如果是传值调用的话,形参是实参的一份临时拷贝,形参和实参在不同的栈帧中,形参的改变并不会影响实参。
但是如果是引用的话,形参实参公用一块内存空间,对形参进行的操作,本质上就是对实参进行的操作
2 大对象传参可以提高效率
因为共同一块内存空间,所以不会额外的开辟一块内存空间
做返回值
与传值返回作比较来理解。
对于传值返回来说,返回的是n的一个拷贝
为什么会这样呢
上面这段代码它的栈帧是这样调用的。首先main开辟了一块栈,由main来callcount的栈。N实际上是保存在count栈帧中,因此当count调用结束的时候,n也会被销毁,此时n内部就是随机值了,如果返回的是n的话,属于越界访问,取不到对应数据。因此需要临时拷贝一份,防止在调用count栈帧结束的时候,数据的丢失。对于小对象来说,一般存储在寄存器中,对于大对象来说,则是在count栈帧销毁之前,将n拷贝在main中,这一份临时变量来做返回值
虽然用static来定义的话,会改变n在内存中的分布。但是编译器主要是根据传值调用或者传址调用来确定上面所做的事情的。没什么意义.
因此传值调用同一都会生成一个拷贝来作为返回值。
传引用返回
返回n的别名。虽然越界了但是不会报错。并且能正确输出结果,为什么?存在一个侥幸。
引用返回实际上tmp是n的一个别名,count调用结束的时候,栈帧也会自动销毁。
因此ret的结果是未定义的。如果栈帧结束时候,系统会清理这一块空间,那么就会被设置成随机值,返回的tmp也就是随机值。但是如果没有上述的清理工作,虽然栈帧已经被销毁了,但是仍然可以找到之前的数据。这取决于系统。所以说这一行为是未定义的。
所以不推荐这样使用
如果是对tmp再去取别名呢?
相当于n tmp ret三个名字都指同一块内存空间。
第一次打印正常,第二次会出现随机值。
为什么第二次打印的时候会变成随机值?
第一次打印出1只是巧合。只是说明恰好count栈帧内的内容没有被修改。但是如果再次去调用另外的函数,就会需要再次建立栈帧,如果覆盖了之前count栈帧中的内容的话,就取不到对应的值了,会变成一个随机值
总结:因此出了函数作用域,如果对象就会被销毁,那么就不能使用引用返回,得使用传值返回。
static修饰,将原本应该存在栈区上的变量放在静态区中。变量不会销毁,可以使用引用返回。
总结:
做参数的话在任何时候都可以,但是做返回值的话,只有出了作用域不会被销毁的情况下才可以使用引用返回。二者都可以提高效率。
对比指针和引用
引用必须初始化,指针不一定,并且指针可以初始化成空值,后续也可以对指针进行修改,但是引用不可以。存在多级引用但是多级指针不存在。
对于sizeof,引用计算实际变量的大小,指针大小为4或者8个字节
对于+1,引用给值+1,指针跳过一个变量
对于解引用,指针要显式解引用使用
引用更安全,不会出现野指针这样的问题
在语法的角度来说,引用没有开空间,指针开了对应4或者8字节的空间
但是从底层实现来说,他们的编译代码并没有多大的区别,可以说是一样的
但是指针和引用都有互相无法代替的场景。
比如定义链表的结点的时候,就只能用指针。因为无法知道下一个结点,那么如何取别名呢?
比如修改链表或者顺序表中某个值,引用可以直接修改
六 内联函数
在c语言中,可以用宏定义来实现替换的功能。宏函数具有复用性高,能提高效率
但是内联函数对宏定义的缺点进行了优化,同时具有相应的优点。比如改变了宏可读性差(下面这段代码就说明了,因为他比较复杂很容易写错,忽略括号的匹配问题),没有类型安全检查(int double不同类型的数据进行传参)以及不方便调试(直接被替换掉了)的缺点。
比如对于add的宏函数:
以inline修饰,在编译的时候,c++编译器会再调用内联函数的地方展开,没有函数压栈的开销了,所以提升了程序的运行效率,但是占用了更多空间
宏函数要注意括号匹配的问题
#define add(x,y) ((x),(y))
add(1, 2) * 3;
add(x | y, x& y);
比较短小并且调用频率非常高 可以设置成内联函数
Inline函数在符合条件的情况下,可以在调用的地方进行展开
因此在c++中不建议使用宏尽量使用const enum inline来替代宏
注意:
1 本质上是以空间换取时间的做法
2如果有递归或者替换之后函数内部实现代码的指令长度比较长inline的建议会被编译器忽略。每一个使用函数的地方都需要被替换,那么占据了太多空间了。编译出来的可执行程序变大
因为每一次调用的地方都是会被展开的,这样子程序就变大了。虽然不展开的话,每次去调用要去call函数并且进行栈帧创建和销毁等相关的操作,速度上稍微差了一点。
所以综合上述考虑,当函数内部的指令比较长就不张开了。内联函数对于编译器来说只是一个建议
3内联函数不建议声明和定义分离。否则链接错误
假设声明和定义分离了,当有文件去调用这个函数的时候,因为只有对于这个函数的声明,所以要去符号表中找对应的定义。但是inline函数直接生成对应的指令,不会在符号表中放入自己的对应函数明和地址。导致这个文件就找不到对应的定义了,导致了链接错误
七 Auto关键词
用于自动推导变量
Auto a =…… 可以根据后面数据的类型 自动决定前面的
Auto*强调变量一定是个指针
Auto&强调变量一定是一个别名
应用场景
1:范围for。Auto&的话,如果是范围for的话,可以达到修改的目的,单纯使用auto则不行
int a[] = { 1,2,3,4,5,6,7,8,9,10 };
for (auto e : a)
{
cout << e << " ";
}
可以省略对边界的判断
但是如果一个函数传参的时候传的是数组,希望通过auto在内部输入输出数组,是不行的。因为本质上传给auto的是指针。
void for (int a[])
{
for (auto& e : a)
{
cout << e << endl;
}
}
2 类型比较长的时候也可以用auto自动匹配
注意:
Auto不能作为函数的参数
因为函数的调用需要建立对应的栈帧,如果是auto的话,不能知道它的大小,不能根据他的实际类型进行推导了
也不能用来声明数组
也不能一行多个
八 nullptr
NULL在c++中会被认为是0,无法与int的类型相区分
因此提出了nullptr这个概念,她的类型是(void*) 0
并且是一个关键字