C++复习
c++概述与基础
1、面向对象与面向过程
C语言是面向过程的语言不利于程序的复用性和扩展性,面向过程的优点:性能高
OOD
:面向对象设计
OOA
:面向对象的分析
C++是一门面向对象编程的语言,优点就是易维护,易扩展,可以设计出更加低耦合的系统,是系统更加灵活,更加容易维护,缺点就是类调用的时候需要实例化比较消耗资源,性能比面向过程低
面向对象的三大特性:封装
,继承
,多态
程序 = 算法 + 数据
2、输入-输出
C++中标准的输入输出是cin
和cout
,定义在标准的iostream头文件中,使用方法:
- 包含头文件
- 打开标准命名空间
#include<iostream>
using namespace std;
cout输出的时候需要结合<<
操作符,本质上是一个函数,endl
是一个插入换行,并且刷新输出流
cin输入的时候要结合>>
操作符,相比于scanf函数来说,不需要对变量进行&地址操作
3、命名空间
在C语言中,如果局部变量和全局变量重名的时候,在局部使用该变量的时候,全局变量失效,这就是变量的临近原则,但是在C++中提供一种作用域运算符号::
,可以通过作用域运算符来访问全局变量,::
成员就指定成员的归属范围,特别的如果::
前不加任何作用域就代表取全局的作用域
namespace:C++中的关键字,用于定义命名空间,主要用于区分同一个作用域下的相同成员
namespace 命名空间名{
变量
函数
结构体
};
使用命名空间的方法
- 使用using 编译指令来打开命名空间,打开一次里面的成员将全部对外开放
- 显示指定命名空间成员,使用
命名空间::成员
来访问指定空间的成员
4、动态申请空间
在C语言中使用malloc
和free
两个函数来申请和释放堆区的空间,在使用malloc函数的时候需要指定申请空间的大小,但是使用的时候需要包含所需的头文件
在C++中提供了两个关键字new
和delete
来动态的申请,释放空间,不需要指定空间大小,只需要指定类型
new int(0);//申请对应类型的空间并且可以给定或者默认的初始值
int* p = new int[3]//new 数组,返回的是首元素的地址
//回收数组的时候需要在指针前加上[]
delete []p;
delete回收的空间并不包含指针本身,而是指针指向的内存空间,同一块内存空间不能重复释放,但是空指针可以,对于栈区的内存空间不能使用delete来释放
new-delete和malloc-free的区别:
- malloc 本身是一个函数,需要头文件的支持,new是c++的一个关键字,不要头文件
- malloc申请空间需要计算类型的大小,返回值是void*,在C++编译器下需要强转,new后面需要放申请空间的类型,返回对应类型的地址,不需要强转
- malloc申请空间时不能指定初始化,new可以指定初始化值
- new在申请结构体,类对象空间的时候,会自动调用构造函数,delete会调用析构函数,而malloc-free并不会自动调用构造和析构函数;
//申请空间练习
//申请一个整型指针
int** p1 = new int*;
delete p1;
p1 = NULL;
//new一个指针数组
int** p = new int* [3]();
delete p;
p = NULL;
//申请一个数组指针
int (* *p3)[2] = new((int*)[2]);
delete p3;
p3 = NULL;
//申请一个二维数组
int (*p4)[3] = new int[2][3];
delete []p4;
p4 = NULL;
//new一个函数指针
void (* *p5)(int) = new (void (*)(int));
delete p5;
p5 = NULL;
5.bool类型
BOOL是windows提供的,并不是C++的关键字,TRUE和FALSE,占4个字节的大小
在C++中提供了一中bool类型,占一个字节,对应真假为true和false
区别:BOOL是整型变量的别名,占4个字节,bool占一个字节
6.string类型
在C语言中能表示字符串的有字符数组和字符指针,但是它们都有它们的局限性
#include<iostream>
#include<string>
using namespace std;
int main(){
char* p = "abc";
p[1]='B';
//p是指向字符常量区的指针,常量区的值不可以修改所以在运行的时候会发生错误
cout<<p<<endl;
p = "123";
//但是p的指向可以更改
//----------------------------------
char str[10] = "1234";
str[2]= '#';
//str是定义在栈区的字符数组,相当于字符常量区的字符产给栈区字符数组进行了复制初始化操作,通过下标修改的是栈区字符数组的值
str = "abc";
//数组名字是常量,不可以进行赋值操作
//----------------------------------
string s1;
s1 = "abcd";
s1[1]='b';
s1 = "1234";
//c++提供了一种字符串类型,结合了字符指针和字符数组的特性
return 0;
}
字符串操作的几个方法:
+
可以实现字符串的拼接操作str.substr(下标,长度)
可以截取字符串,如果长度超限,则会截取有效长度,下标越界的话运行会异常==
可以判断两个字符串是否相等,相当于C语言中的strcpy这个函数str.length()
和str.size()
可以返回字符串的长度str.c_str()
可以将string中的字符串转换为==const char*==并返回
6.增强的范围for
C++11提供了一种增强的for循环,适用于普通数组,string,和支持begin,end操作的容器
int arr[5] = {1,2,3,4,5};
//传统的for循环
for(int i = 0;i<sizeof(arr)/sizeof(arr[0]);i++){
cout<<arr[i]<<endl;
}
//增强for循环
for(int v:arr){
cout<<v<<" "<<endl;
}
string str = "abcd";
for(char c : str){
cout<<c<<endl;
}
7.函数重载
函数参数指定默认值顺序:**从右向左*依次指定,中间不能间断
函数声明和定义一般分开写,在声明的地方写默认值,分开写可以解决相互调用的问题
C++中函数的名字可以相同,但是参数列表不同(类型,数量,顺序),返回类型可同可不同(在同一个作用域下)
重载的函数在调用时候,可以根据参数列表调用相应的函数
8.空指针
指针初始化为空的时候我们有两种方式:
使用NULL
和nullptr
(C++11新引进的)
两者的区别:
- nullptr是C++关键字,NULL是一个宏 替换 0
- nullptr代表空指针,NULL代表数字
为什么还要使用nullptr呢?
在函数重载时,0和空指针整型含义不明确的问题(混用的问题)
9.引用
引用
对一块已存在的空间起了一个别名
如果要修改实参,不能使用值传递,可以使用地址传递和引用传递,推荐使用引用传递,不额外申请空间,指针额外申请指针大小的空间,可控的,值传递申请的空间大小不确定要根据类型才能确定大小;
对于重载函数调用的歧义性问题,可以通过函数指针来调用,这样调用就不会有参数歧义性的问题;
void fun(int a){
//值传递
}
void fun(int* a){
//地址传递
}
void fun(int &a){
//引用传递
}
int main(){
//引用定义就要初始化,也就是不存在空的引用
//引用一旦引用了某个空间,其引用指向就不能修改了
//引用不存在多级引用
return 0;
}
类基础知识
1.类封装
class(类)
完成某一个功能的数据(成员属性)和算法(成员方法)的集合,是一个抽象的概念
对象:
就是类的一个实例,具体的概念,存在于真正的内存中
定义一个类的方法:
class 类名{//类名一般是大写字母C开头,成员一般以m_开头
访问修饰符:
int m_a;//成员属性
void show();成员方法
};<-别忘了加分号,C语言中结构体大括号后面不加分号
1.1类访问修饰符
在类外访问的时候,会有访问的限制,在定义一个类的时候如果不指定访问修饰符的话,编译器默认会初始化为private类型,这样类型修饰的话类外是访问不了的.
三种类访问修饰符:描述类中成员的访问权限
private:
私有类型,只能在类内使用;
protected:
保护类型,在类内和类外可以访问
public:
公共类型,对外公开,类内类外都可以使用
1.2构造函数
通常在定义变量的时候都要初始化默认值,定义类对象也是,C++提供了一种特殊的函数来初始化类对象-构造函数
*构造函数:*用来初始化类成员属性,一个空类中存在一个默认的无参构造函数,构造函数的格式:一般为当前类名(){}
构造函数不需要我们手动调用,在定义类对象的时候会自动调用,一个类中可以存在多个构造函数,也就是说构造函数也可以重载,但是每一个类对象最终只能执行一个构造函数.只要重构了任何构造函数,编译器就不会提供那个默认的无参构造函数了
class CTest{
CTest(){
//参数:编译器提供的则为无参构造
//如果手动重构的话,参数为自己指定的参数
//默认无参构造
//无返回类型但不是void
}
~CTest(){
}
};
1.3析构函数
*析构函数:*与构造函数相对应,作用是用来回收类中成员申请的额外空间,并不是对象本身,空类中默认存在一个析构函数
格式为: ~类名(){}
无参数和无返回值
析构函数在对象生命周期结束的时候会自动调用,析构函数只允许存在一个,析构函数在真正真正回收对象内存空间之前使用,额外的空间回收完之后,才真正回收对象内存空间(先堆区空间最后栈区空间)
1.4POP和OOP再认识
面向过程[POP]:自顶向下,顺序执行,逐步细化
面向对象[OOP]:分析问题中参与的实体,这些实体应该有那些属性和方法,我们如何通过这些实体的属性和方法区解决问题
2.类成员之间的关系
空类
的大小为1个字节,用来占位作用,用来表示当前对象真实存在在内存中(C++标准规定)
成员属性:属于对象,而不属于类,当定义对象的时候属性才会存在,才会开辟对应的空间,多个对象会存在多份成员属性,彼此独立存在互不干扰;
成员函数:属于类的,编译器存在,一个类只会存在一份,它的存在与是否创建对象无关.不占类的空间,而是整个程序的空间,
类中一般的成员函数,默认会存在一个隐藏的参数,this指针,类型为:类名 + * + const this
(如CTest* const this
),编译器默认添加的第一个参数
==this指针的作用:==指针指向了调用该函数的对象,函数中使用的类成员都是通过this指针去调用的,连接对象和成员之间的桥梁,可以在函数非常方便的使用成员
2.1静态成员
静态成员属性:
属于类的(而不是对象的),编译期就存在,一个类中只存在一份,多个对象之间公用这一份静态成员属性
静态成员属性需要在类型进行初始化初始化格式:类型 类名作用域 变量名 初始值
静态成员也不能在初始化参数列表中初始化
类外初始化的时候要去掉static关键字
也可以通过类名作用域直接调用,也可以通过对象去调用,静态变量的存在与是否定义对象无关,在构造函数中不能对其初始化(构造函数中的为赋值操作,因为构造函数声明周期是在定义对象的时候才会创建,而静态成员变量在编译器就存在了)
2.2静态成员函数
静态成员函数
:属于类的,编译器就存在,一个类只存在一份,多个对象公用一份静态成员函数
区别:
- 本质区别静态成员函数没有隐藏的this指针,所以不能调用普通成员属性和方法
- 静态函数只能使用静态成员
- 普通成员函数和对象则可以调用静态的成员和函数
- 静态成员函数可以不通过对象去调用,可以通过类名作用域去访问,普通函数必须通过对象去调用(不能使用类名作用域直接调用)
2.3常量成员
常量的特性:
一经定义后就不能修改,定义就必须要初始化
定义的时候就必须初始化
要在初始化参数列表中初始化
初始化参数列表:用来真正初始化对象中成员属性,构造参数列表后要加冒号,多个成员属性之前用逗号分割,初始化参数列表的顺序取决与成员在类中声明的顺序
2.5常函数
指针升级降级的问题:
在指针类型转换的时候遵循一个原则就是说,安全等级低的指针可以向安全等级高的指针转换,但是反之则不可以
如:
int* -> const int*
but!!!
const int* !-> int*
常函数:当类成员函数参数列表后(函数名字)有const修饰的时候,这时候就被称为常函数void fun() const{}
**目的:**保护类中的成员属性和方法不能被修改(本质上被const修饰类成员函数变成了const CTest* const this
)
this指向的内容不能修改了,带来的改变就是不能修改类中非静态成员,但是可以查看成员变量,但是常函数中不能修改成员变量,对于静态成员可以查看也能修改
常函数可以调用其他常函数,但是不能调用普通函数
原因:
- 静态成员不属于对象,而是属于类的,不在const修饰的范围之内
- 调用普通函数是指针安全等级降级的操作是非法的
常函数和普通函数的区别:
- 常函数的this指针为:
const CTest* const this
普通函数的this指针类型为类 * const this
常函数调用普通函数是指针安全等级降级的操作是非法的 - 常函数(一般不能修改)不能修改类中的成员属性(可以通过指针,或者关键字
mutable
来修改),也不能调用一般函数,一般函数可以修改成员变量,也可以调用常函数
3.类之间的横向关系
3.1组合(复合)
是一种“pat of”的关系,部分和整体,包含和被包含,组合关系的两个对象往往具有相同的生命周期,被组合的对象是在组合对象创建的同时或者创建只会创建的,在组合对象之前销毁:比如人和手;C++中通常在组合类中包含组合类对象来实现组合关系
人和手之间的关系
在C++中一般表达组合关系就是类包含
3.2依赖
是一种“use of”的关系,一个对象依赖于另外一个类对象,被依赖的对象视为完成某个功能的工具,依赖之间没有生命周期关系,C++中,通常以被依赖的对象作为另一类的方法的参数形式实现两个类之间的依赖关系
依赖的表达方式就是:
在类的成员方法中,将另外一个类的指针当参数传进去
3.3 关联
是一种“has a”的关系,是平等关系,可以拥有对方,但是不可以占有对象,关联可以共享C++中关联的类对象的指针形式实现两个类之间的关系
在类中使用另外一个类的指针
3.4 聚合
是一种“owns a”的关系,多种聚合的对象形成一个大的整体,聚合的目的 就是为了同一管理同类型的对象,聚合是一种弱从属的关系被聚合的对象还可以被别的对象关联所以被聚合的对象是可以共享的C++中,聚合类定义在被聚合的对象的数组、链表等容器中
4.类之间的纵向关系
4.1继承
继承:类和类之间的纵向关系,子类继承父类,可以使用父类的成员和方法,也会包含父类;
被继承类叫做父类(基类),继承的类叫子类(派生类)
继承的写法:
class CFather{};
class CSon:public CFather{};
通过继承关系,子类可以使用父类的成员
如果父类和子类有重名的成员,那么子类对象默认会使用子类成员,如果想使用父类的成员可以通过对象.类名作用域来访问父类的成员
对于父类和子类函数同名时,父类的函数会被屏蔽这种现象我们称之为隐藏特性(与成员重名规律一样)
内存布局:父类空间在前面,子类空间在后面,排序顺序父类-子类自上而下
内存大致布局:
内存首地址
>m_Fa
>m_Fb#####父类成员存储空间########
子类对象->
>m_Sa
>m_Sb#####子类成员存储空间########
初始化成员属性:父类成员在父类中初始化,子类成员在子类中初始化
构造函数执行的顺序:先父类构造然后子类构造|子类析构父类析构
定义子类对象,优先调用的是子类的构造,在子类的初始化参数化列表初始化父类和子类的成员,先调用父类的构造函数初始化父类成员,在初始化子类(同内存的排布顺序一致)
子类构造初始化参数列表,由编译器默认会带哦用父类无参数构造,如果向调用父类带参数的构造,或者父类没有无参数的构造,则必须显示指定父类构造
析构顺序:先是子类析构函数,后是父类的析构函数,
定义子类对象,当子类对象生命周期结束时,优先匹配子类的析析构函数,回收玩子类申请的额外空间后再回收对象本身的空间,子类对象中包含了父类的匿名对象,再回收这个匿名对象前调用父类的析构,再回收父类的匿名对象
继承的优点:
- 将一些功能比较相似的类中的公共成员抽离出来,封装成一个父类,减少代码冗余,提高代码复用性、扩展性
继承方式
: 描述了父类成员再子类中表现的属性和访问修饰符共同决定了访问控制和权限
public:公共继承
父类 | 子类 |
---|---|
public | public |
protected | protected |
private | 不可访问 |
protected:保护继承
父类 | 子类 |
---|---|
public | protected |
protected | protected |
private | 不可访问 |
privated:私有继承
父类 | 子类 |
---|---|
public | private |
protected | 不private |
private | 不可访问 |
在继承关系下父类的指针可以指向子类的对象,但是反过来却不可以,这种特性可以是父类统一多个子类类型,从而提高代码的复用性和扩展性
this指针
- this指针指向被调用函数所属的对象
- this指针可以解决名称冲突问题
- this指针默认隐藏在非静态的类成员函数之中(
CTest const* this
) - 当要返回类对象的引用的时候**this就是本体*
空指针来访问类成员函数
#include<iostream>
using namespace std;
class CPerson{
public:
int m_age;
void showPeo(){
cout<<"name is person"<<endl;
}
void showAge(){
cout<<"age:"<<m_age<<endl;
}
};
int main(){
CPerson* peo ;
//空指针是可以调用函数的,实质上它本身没有用到this指针
peo->showPeo();
//但是空指针无法访问虚函数
return 0;
}
总结:
- 如果成员函数中没有用到this指针,那么可以使用空指针去调用成员函数
- 如果成员函数中用到了this,那么这个this需要加判断
友元
- 全局函数作为友元函数,可以访问到类中的私有成员属性
多态
多态:相同的行为方式可能会导致不同的行为结果,也就是产生了多种形态行为,称为多态.
多态的本质:定义父类的指针可以指向任何继承于该类的子类对象,且父类的指针具有子类对象的行为,多种子类表现为形态由父类的指针进行统一;
C++直接支持多态的条件:
- 必须存在继承关系(前提),存在父类的指针指向某个子类,通过该指针调用虚函数
- 父类中存在虚函数(virtual)且子类中重写了父类的虚函数
class CFather {
public:
virtual void show() {
cout << "CFather::show()" << endl;
}
};
class CSon:public CFather {
public:
//重写了父类的虚函数
void show() {
//即使省略了virtual也是虚函数
cout << "CSon::show()" << endl;
}
};
虚函数-虚函数指针-虚函数列表
定义虚函数的格式:virtual +类型 +函数名()
虚函数是实现多态必不可少的条件之一
__vfptr
:虚函数指针,是编译器默认添加的一个二级指针,类型为(void**),属于对象的,占用对象的内存空间,定义多个对象就会有多份虚函数指针,作为编译器默认添加的属性,在构造函数的初始化参数列表中由编译器自动做初始化,它指向的一个void* 的数组
什么条件下会添加虚函数指针?
当类中存在虚函数,就会默认添加
虚函数列表:本质上是虚函数的函数指针数组
每一个元素存储的都是虚函数的地址,
顺序就是虚函数在类中定义/声明的顺序
属于类的,在编译期就存在了虚函数的调用流程:
定义对象,找到对象中的虚函数指针
通过这个指针间接引用
找到虚函数列表
通过下标定位到具体调用的虚函数地址
获取地址后通过地址来调用虚函数
多态下虚函数指针指向子类的虚函数列表,主要看对象的的类型(new后面的具体对象),而不是看指针的类型
继承下的虚函数(子类虚函数列表):
- 子类继承父类,也会继承父类的虚函数列表
- 检查子类是否重写父类的虚函数,如果重写,就会替换掉父类的虚函数(在原位置替换)如果没有重写,父类虚函数任然保留
- 如果子类有单独的虚函数,按照声明定义顺序依次添加到虚函数列表尾部
虚函数和普通函数的区别:
- 调用流程不同
- 效率不同,虚函数调用流程复杂,效率慢,普通函数效率高
- 使用场景不同,虚函数的目的是为了实现多态
多态:运行期多态,是动态的多态,虚函数调用是动态的绑定
多个对象的多份虚函数指针指向的是同一个地址(同一个数组)
父类的指针可以统一多种子类类型,减少代码的冗余。提高程序的复用性和扩展性;
但是父类的指针在一般继承下只能使用父类成员不能使用子类成员
全局函数和类成员函数的不同点:
- 作用域不同
- 类成员函数有隐藏的this指针,全局函数没有this指针
::*是一C++中整体操作符,作用是定义 类成员函数指针
.* /->*也是C++中的整体操作符,作用是通过对象调用类成员函数指针指向的函数
**虚析构:**虚析构是对父类的重写(编译器优化的)
delete 调用那个类的析构取决于后面放的指针类型 于指针指向的对象的类型无关
子类析构没有执行,子类中额外存在的空间没有释放,可能导致内存泄漏
抽象类:包含纯虚函数的类称为抽象类,抽象类不能定义对象
接口类:所有虚函数都是纯虚函数
类成员函数指针
在函数中存在两种调用方式
- 通过函数名字直接调用
- 通过函数指针间接调用
使用函数指针调用的优点:可以将实现同一功能的多个模块统一起来标识,使得系统结构更加清晰,后期更容易去维护,便于分层设计,有利于系统抽象,降低代码的耦合度,利于系统抽象,从而提高代码的复用性和扩展性
Q:如何定义一个函数指针
void show(int a,char c){
}
//找到函数声明,去掉函数名字替换为()
void ()(int a,char c)
//在括号内加上* 和去掉参数的名字只保留参数类型
void(*)(int,char)
//最后加上函数指针的变量名
void(* pfun)(int,char) = &show;
//这样就定义了 一个指向show函数的指针
//接下来就可以通过调用函数指针去间接调用函数了
类成员函数和普通函数的区别:
- 所属的作用域不同,类成员函数必须通过对象调用(静态函数除外)
- 参数不同:类成员函数编译都会默认加上一个隐藏的参数(this指针)
- 类成员函数指针有三种不一样的运算符`:😗 .* ->.**用来定义和使用类成员函数指针,这是普通函数没有的
多态
编译时多态(静态)和运行时多态(动态多态)
多态:相同的行为方式导致了不同的行为结果,同一行语句展现出多种不同的表现形式,即多态性
class Animal
{
public:
//Speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编
void DoSpeak(Animal & animal)
{
animal.speak();
}
//
//多态满足条件:
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象
void test01()
{
Cat cat;
DoSpeak(cat);
Dog dog;
DoSpeak(dog);
}
int main() {
test01();
system("pause");
return 0;
}
在继承的条件下,父类的指针可以指向任何继承于该类的的子类对象,多种子类具有父类指针指针统一管理,那么父类的指针也会具有多种形态
多态的条件:
- 在继承条件下,父类指针指向子类对象
- 父类中存在虚函数(virtual),子类重写(虚函数存在条件下,子类定义了和父类名字一模一样的函数)父类的虚函数
虚函数
在类成员函数中,被关键字virtual
修饰后的函数称为虚函数
当一个类中定义了一个虚函数的时候,在定义的对象空间的首地址会多分配出一块内存标识这块内存的变量叫做虚函数指针__vfptr
- 普通成员函数是属于类的,而虚函数是属于对象的;多个对象会分配多个指针,
- 本质是一个二级指针,每个指针里面保存是虚函数列表(vftable)
- 虚函数在构造函数的初始化参数列表进行初始化(由编译器默认完成)
- 虚函数列表本质上是一个指针数组,里面保存的是虚函数地址,在编译期存在,所有对象共享,必须通过真实存在的对象才能调用虚函数
虚函数调用的流程
step1:当定义对象的时候,虚函数指针就会被初始化
step2:通过对象可以找到内存首地址中的虚函数指针
step3__vfptr就可以找到虚函数列表,每个指针数组里面保存的就是虚函数的地址
虚函数与普通成员函数的区别:
- 调用流程不同,虚函数调用流程比普通函数更加复杂
- 调用效率不同,普通成员函数调用效率高,虚函数,调用效率低,速度慢
- 使用场景不同,虚函数主要用来实现多态,而普通成员函数不可以
多态实现的原理
子类继承父类,也会继承父类的虚函数列表
虚函数指针属于对象,每个对象都有自己的虚函数指针,虚函数列表是属于类的也就是父类和子类都有它们的虚函数列表
编译器会检查子类中是否重写了父类的虚函数,如果有,那么子类的虚函数列表会替换掉父类的虚函数列表(覆盖),如果没有,父类的徐韩素华会被保留在子类的虚函数列表中
子类定义的独有的虚函数会按顺序依次添加到虚函数列表的结尾
以上流程在编译期完成
虚函数指针在那个子类的初始化,就指向子类虚函数列表,盛情那个子类对象指针就会指向那个子类的虚函数列表,调用虚函数执行虚函数的调用流程,就实现了多态
多态的缺点:
- 效率问题:多态是通过虚函数实现的,虚函数调用流程复杂,效率低
- 空间问题:虚函数指针占用指针大小空间,且每个对象都会存在一份,虚函数列表会随着继承的层次增多,其大小只增不减
- 安全问题:如果一个函数是私有函数,则可以通过函数指针的方式来进行类外访问,所以建议如果一个函数是私有变量就不要称为虚函数了
头文件-源文件
头文件:写变量的声明,函数的声明,类的声明等等所共享的代码
源文件:变量的定义,函数的定义,类成员的定义实现等
类成员函数在类外定义的时候一定要加上类名作用域
单独的头文件不参与编译,多个源文件的话自上而下编译
头文件的重复包含:
#pragma once
:告诉编译器当前的头文件在其他源文件中只包含一次(相对效率比较高)
基于逻辑宏的判断:当有大量头文件的编译速度降低,耗时增加;
#ifndef 宏名字
#define 宏名
your code
#endif
用以上宏结构来解决头文件重复包含的问题,第一种方便,效率块,第二种程序编译的效率慢,宏名字会重复,导致使用的时候会出问题
*头文件:*通常我们把变量的声明,类型函数,宏,结构体和类的定义放在头文件,把变量的初始化,函数的实现放在源文件中,这样可以方便我们管理规划。
需要注意的是类成员函数在类外初始化的时候要加上类名作用域,静态函数的初始化要去掉关键字,虚函数也要去掉关键字,常函数要保留。
程序的生成过程
- 预处理(preprocessing),头文件展开替换、宏替换、预处理指令,删除注释,.cpp->.i文件
- 编译(compilation)将预处理后的文件进行语法语义分析及优化,生成相应的汇编代码文件(.asm).i->.s
- 汇编(ASSembly)将编译后的汇编文件(.asm)汇编指令逐条翻译称为目标机器指令,并生成可重定位的目标程序的.obj文件也就是二进制文件,字节编码是机器指令
- 链接(linking)通过链接将多个目标文件和库文件链接在一起生成一个完整的可执行程序
编译器-运行期
*编译期:*将源代码交给编译器,编译,生成可执行程序的过程(exe)
类是编译期的概念,包含了【访问权限】,【成员作用域】,对象的作用域是运行期,它包含类的【实列】,【引用】,【指针】
在虚函数实现多态的时候,在编译期的时候pFa->虚函数是调用父类的属性,但是在运行期由于多态的作用,调用的是子类中的虚函数,即使子类的函数是私有属性,但是由于访问修饰符,是编译期的限制,所以在运行的时候无效的,子类的私有属性也就可以调用了,这样就会引发安全问题,一般建议私有属性不建议设为虚函数
*运行期:*指将最终的可执行文件交给操作系统执行,知道程序退出,把磁盘中二进制代码放到内存中执行起来,执行的目的是为了实现程序的功能
宏的用法
1.替换作用
#define N 10
2.带参数的宏替换
#define N(parm) int a = parm;
3.替换多行
#define N(num)\
for(int = 0;i<num;i++)\
{\
cout<<i<<endl;\
}
4.#undef 可以卸载宏,限制宏的作用范围
5.#用于将宏参数转换为字符串也就是加上了字符串
#define N(parm) #parm
N(abc) //=="abc"
6.#@用于将宏参数转换为字符,加上单引号
#define N(parm) #@parm
N(A) //=='a'
7.##用于拼接,常用宏参数与其他内容拼接
#define N(parm) int a=##parm
//int a =
宏处理的优缺点
1.使用宏可以替换程序中经常使用的常量或者表达式,在后期维护修改的时候不用对整个程序修改,只需要修改一份宏即可
2.宏可以替换一些简单的函数,这样就省去了调用函数的开销,提高了程序运行的效率
缺点:
1.不能调试,没有类型的安全检查(宏默认没有类型)
2.对带参数的宏也是直接替换,并不会进行语法检查,具有安全隐患
内联函数
内联函数是C++为了提高程序运行速度所做的一种改进,与普通函数不同的是编译器会将相应的函数代码替换到内联函数的调用处,而不需要跳转到另一个位置执行代码,代价就是需要更多的内存,是以空间换时间的做法
内联函数的注意事项:
1.内联函数的思想是以空间换时间的做法,在一定程度能够提高程序运行的速度,但是如果函数调用的时间远远小于函数体代码执行的时间,那么效率提高并不多,并且该函数被大量调用的时候,每一处调用都将会复制一份函数代码,那么占用的内存就会增加,这种情况就就得不偿失了,所以一般函数体代码较长,函数体循环等不推荐作为内联函数
2.其次内联函数是程序员对编译器提出的一个建议,并不是强制性的,编译器,有自己判断,他会根据具体情况来决定是否把他设为内联函数
3.类,结构体中在类内部声明并定义的函数默认为内联函数,如果类中只声明,在类外定义的话就不是内联函数了,除非手动加上关键字,
重载操作符
重载操作符,本质上是一个函数,告诉编译器当遇到这个操作符函数的时候,能够匹配重载的这个操作符函数,通过函数实现了这个操作符的功能,一般要有返回值,主要是为了和后续的操作符继续操作,(对操作符进行扩展和补充,而不是创造新的操作符)
operator:
关键字后面加上要重载的操作符
注意参数匹配(类型,顺序,数量)
运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能以适应不同的数据类型.
类外重载和类内重载:
-
同一个操作符在类外重载会比类内重载的函数,多一个参数
-
类外重载函数参数顺序可自定义设定,类内参数顺序相对来说比较固定
-
类内和类外重载操作符函数注意是否产生二义性的问题
在重载中为了区分左++和右++,右++参数列表里面会多一个参数起区分作用,没有实际意义
只能在类内重载的操作符:=,[],->,()
在类外重载操作符,必须至少得包含一个自定义类型(类,结构体)
重载操作符参数不能有默认值,
同一个操作符,参数不同,可能会代表不同的函数
在重载操作符和重载类型都能匹配得情况下,优先级是重载操作符的优先级高
list-map的简单使用
list链表
常用的一个封装函数被
list.push_front()//头添加
list.push_back()//尾添加
list.pop_front()//头添加
list.pop_back()//尾添加
iterator ite;链表的迭代器
ite = list.begin()//让迭代器指向链表的头
while(ite!= list.end(){
//使用迭代器来遍历链表
ite++;//迭代器的作用就是可以通过指针来使用链表
}
ite2 = lst.insert(ite,value)//链表插入,返回插入元素的迭代器
lst.erase(ite)//删除ite指向的节点,返回值删除节点的下一个节点
list.front()==ite= lst.begin()//获取首节点的值
list.back()//获取尾节点的值
end()返回的是无效的尾节点,不能对其间接引用
lst.size()//返回链表的长度
lst.empty()//返回链表是否为空
lst.clear()//清空链表
//使用之前要包含<list>头文件
list<int> lst;
lst.push_back(1);
lst.push_back(2);
lst.push_back(3);
lst.push_back(4);
/*lst.pop_back();
lst.pop_front();*/
list<int>::iterator ite = lst.begin();
/*while (ite!=lst.end()) {
cout << *ite << endl;
ite++;
}*/
lst.push_back(5);
lst.push_back(6);
ite = ++lst.begin();
//在指向的位置之前,返回值是新插入节点的迭代器
list<int>::iterator ite1
=lst.insert(ite,30);
cout << "*******************"<<endl;
cout << *ite1 << endl;
/*while (ite != lst.end()) {
cout << *ite << endl;
ite++;
}*/
for (int v :lst) {
cout << v << endl;
}
map映射表
map是一种映射表,每一个元素被称为键值对pair
分为:键值(key),实值(value),键值唯一且不能重复,可以根据键值自动排序(默认是升序);
拷贝构造
*转换构造函数:*可以让构造函数发生隐式转换的函数称为转换构造函数,如果想避免隐式类型转换,则需要在构造函数前加上关键字explicit
(修饰构造函数,禁止发生隐身类型转换,必须收订显示传递)
class CTest{
public:
int m_a;
int* p;
expilcit CTest(int a):m_a(a),m_p(new int(10)){
}
//在空类中默认会存在以下函数
//参数为const当前类对象的引用,返回类型也是当前类对象的引用
//编译器默认提供这个函数体代码,形参对象中的成员依次会给this指针做赋值操作
CTest& operator=(const CTest& tst){
//手动实现深拷贝
if(this != &tst)
{
this->m_a = tst.m_a;
if(this->m_p)
{
//都有自己的空间
*this->m_p = *tst.m_p
}
else
{
//如果没有
this->m_p=new int(*tst.m_p);
}
else{
if(this-m_p)
{
delete m_p;
m_p = nullptr;
}
}
}
this->m_a = tst.m_a;
return *this;
}
//一旦我们手动重构这个函数,编译器就不会提供这个函数了
//这个函数默认是一个浅拷贝,也会有浅拷贝的问题,需要手动实现深拷贝
};
拷贝构造函数:编译器默认提供的一种特殊的构造函数,与空类中默认的无参构造并存
格式:const+当前类名+& 对象
编译器默认提供,函数体代码不为空,形参中的类成员,依次会给this对象中的成员做初始化操作,一旦重构了拷贝构造函数,编译器就不提供默认的拷贝构造函数了
浅拷贝:编译器默认提供的拷贝函数是一个浅拷贝,在类中存在指针成员并申请堆区空间的时候,回导致多个对象中的指针成员指针指向了同一块空间,回收的时候会被回收多次,导致程序发生异常
深拷贝:需要手动重构构造函数,为当前对象中的指针单独开辟一块空间,并将值也拷贝过来
空类中默认提供的函数:
- 默认无参的构造函数
- 默认的析构函数
- 默认无参的拷贝构造函数
- 默认的operator=
设计模式
所谓设计模式就是前人总结的一些经验规则,被反复使用后形成的一套代码设计经验,设计模式的主要目的是为了解决某类重复问题而出现的一套成功的解决方案,提高了代码的复用性,扩展性,可维护性,稳健性,以及安全性的解决方案。
设计模式一般分为3类:
- 创建型模式
- 结构性模式
- 行为型模式
单列模式(Singleleton Pattern)
- 当前的类最多只能船舰一个实列
- 当前这个唯一的实列:必须有类的自助创建,而不是调用者创建
- 必须指向整个系统的提供的全局访问点来获取唯一实例
class CSingleton{
private:
//让构造函数变为私有的
CSingleton(){
}
//设置一个静态成员函数指针
static CSingleton* pSingleton;
public:
//提供一个公共的访问接口、
static CSingleton* GetSingleton(){
if(!pSingleton){
pSingleton = new CSingleton;
}
return pSingleton;
}
};
//类外初始化
CSingleton* CSingleton::pSingleton(nullptr);
懒汉式:当第一次调用接口函数的时候,现创建单例,是以时间换空间的思想
饿汉式:无论是否调用获取单例接口,都会提前创建,是以空间换时间的做法
单例模式优点:
- 单列模式提供了严格的对唯一实例的创建,访问和销毁,安全性高
- 单例模式可以节省系统资源
工厂模式(Factory Pattern)
主要用来集中创建对象,如果在任意地方创建对象就造成了类和方法之间的耦合,并且不利于后期的维护,也违背了开闭原则,使用工厂模式可以解耦合
工厂方式总结:
- 简单工厂适合种类比较少,且比较固定的对象(违反开闭原则)
- 工厂方法的模式对应一个具体类型的对象,遵循开闭原则,提高了扩展性
- 抽象工厂
类模板
*泛型编程:*将算法从数据结构中抽离出来,用不变的代码完成一个可变的算法,核心就是屏蔽数据和操作数据的细节,让算法变得更加通用
template:定义模板的关键字
typename:定义模板类型的关键字
< >:模板的参数列表
T:通用类型的标识符
定义格式:template
函数模板:它的数据类型可以用一个虚拟的(模板)先代替,在实际调用时,编译器根据传入进来的实参退出真正的类型,生成对应具体的函数
//模板函数被,在调用函数的时候可以通过实参自动推导
//实参自动推导类型
template<typename T>//T为一个通用数据类型
//告诉编译器后面函数或者类中出现T不要报错
T add(T a,T b) {
return a + b;
}
//显式指定
template<typename T>
void fun1() {
T a = 0;
cout << typeid(a).name() << endl;
}
//指定默认类型
template<typename T = int >
void fun2() {
T a = 0;
cout << typeid(a).name() << endl;
}
//三种使用方式的优先级
template<typename T = int >
void fun3(T t) {
t = 0;
cout << typeid(t).name() << endl;
}
//优先级:
//显式指定->实参自动推导->默认模板类型
使用方法:
- 常规使用方法(编译器根据参数自动推导类型):建立一个函数模板,类型和参数都用模板来替代,在函数调用的时候由函数模板生成具体型的函数,编译器由函数模板自动生成模板函数的过程叫做模板的实例化
- 显式指定默认类型或者默认值
- 函数模板指定的默认类型,没有强制的顺序要求
- 按需实列所需类型的函数,编译单元文件内按需实列化,建议声明和定义放在一起使用(就不会报linkerro错误了)
优先级:显式指定->实参自动推导->默认模板类型
//用模板实现一个交换函数
template<typename T>
void mySwap(T& a,T& b){
T temp = a;
a=b;
b =temp;
}
//写一个通用的排序算法函数
template<typename T>
void mySort(T arr[],int len){
for(int i =0;i<len;i++){
int max = i;
for(int j = i+1;j<len;j++){
if(arr[max]>arr[j]){
max = j;
}
}
if(i != max){
mySwap(arr[i],arr[max]);
}
}
}
template<typename T>
void myPrint(T arr[],int len){
for(int i =0;i<len;i++){
cout<<arr[i]<<endl;
}
}
多模板参数
模板参数中可以指定多个,每个参数之间用逗号分割,每个模板关键字都需要typename来修饰
格式:template<typename T,typename K>
同样的,多模板参数也可以根据实参进行自动推导,并且模板参数的指定顺序是任意的没有强制顺序要求,但是在调用的时候需要显式指定模板的时候需要从左向右依次指定,并且不能间断,当有默认类型的时候需要从右向左依次指定且不能间断
使用经验,一般将编译器能够自动推导出来的模板参数放在最后,剩余模板参数有默认值的放在中间,无默认值的放于前面;
函数模板的声明和定义
如果使用了模板的函数声明和定义分开了,那么声明和定义处都需要加上模板,如果模板存在默认类型,那么只需要在函数声明的时候指定即可;
函数模板和普通函数的区别
- 在模板函数中,如果是自动类型推导,那么是不可以发生隐式类型转换的
- 普通函数中,可以发生隐式类型转换
- 如果模板函数和普通函数都可以调用,那么会优先调用普通函数
- 如果想强制调用函数模板,可以使用空模板参数列表(<>)
- 函数模板之间也可以发生重载
- 如果函数模板能产生更好的匹配,那么会优先使用模板函数
模板的实现机制
- 函数模板通过具体的类型产生不同的函数
- 函数模板会进行两次编译,第一次在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译
模板的局限性:
编译器并不能把函数模板处理成任何类型的函数,模板并不是真实的通用,对于自定义的数据类型,可以使用具体化技术
template <> bool myCompare(Person &a,Person &b)
类模板
与函数模板差不多,使用的时候也需要在类定义的地方加上template
和typename
关键字,但是在定义对象的时候必须显式<>指定模板类型
- 模板类型可以替换任意地方定义的类型,包括成员属性和成员方法
- 类模板不可以使用自动类型推导需要显示指定(与函数模板的区别)
- 类成员属性是模板类型的时候,可以指定自定义参数的构造函数,让调用者去初始化
- 类模板也可以右多个参数,规则个模板函数差不多也需要从右向左依次指定且不能间断,在定义对象的时候从右向左依次指定
- 当模板类中成员函数在类外定义时,类外实现的时候,定义的函数也需要加上模板
- 当类中使用了模板,并且函数中也使用了模板的时候,并且函数在类外定义的时候,先类模板后函数模板,这个顺序不够反
- 类模板中的成员函数,并不是一开始创建的,而是在运行的阶段确定出T的数据类型后才去创建的
使用实例:
//先类模板,后函数模板,顺序不能反
template<typenameT>//类模板
template<typenameK>//函数模板
void CTest<T>::fun2(K k){
cout<<typeid(T).name()endl;
cout<< typeid(k).name()<<""<<k<<endl;
}
int main(){
//使用方法
CTest<long> tst(20);
tst.fun2(1.1);
}
类嵌套模板
#include<iostream>
using namespace std;
template<typename T>
class AA{
public:
T m_a;
AA(const T& a):m_a(a){
}
};
class BB{
public:
AA<int> aa;
B():aa(1){
}
}
template<typename T>
class CC{
public:
AA<T> m_k;
CC(T a):m_a(a){}
};
template <typename K>
class DD{
public:
K m_k;
DD(const K &k):m_k(k){}
}
int main(){
CC<char> c('a');
cout<<c.m_k.m_a<<endl;
AA<double>aa(12.3);
DD<A<double>> d(aa);
cout<<d.m_k.m_a<<endl;
return 0;
}
动态数组的设计
//动态数组的设计
template <typename T>
class CDynamicArr{
public:
T* pArr;
int mSize;
int mCapacity;
CDynamicArr(int cap = 0):pArr(nullptr),mSize(0),mCapacity(0){
if(){
pArr = new [cap]();
mCapacity = cap;
}
}
~CDynamicArr(){
if(pArr){
delete []pArr;
pArr= nullptr;
mSize=mCapacity =0;
}
}
public:
//添加---尾添加效率高
void pushBack(const T & t){
if(mSize>=mCapacity){
//
}
else{
}
}
};
STL
STL是(standard template library)中文译名为标准模板库,是C++的一部分,以源码的方式提供,体现了泛型编程的思想,大部分的算法被抽象,被泛化独立有与之对应的数据结构,用于相同或者相近的方式处理不同的情形,为开发者提供了一个可扩展的应用框架;一个显著特点就是数据结构和算法的分离;
STL包含六大组件:
- 容器(container)
- 迭代器(iterator)
- 算法(Algorithm)
- 容器适配器(Adapter)
- 空间分配器(Allcator)
- 仿函数(Function Objects)
容器(container)
各种数据结构,如vector,list,deque,set,map,array等,用来存放各种数据结构,从实现上来看,STL是一种类模板(class template)
容器主要分为两大类:
- 序列式容器:序列式容器强调值的排序,序列式容器中每个元素均有固定的位置,除非用删除或者插入的操作来改变位置,如vector,deque,list
- 关联式容器:关联式式容器,他是非线性的结构,更准确的说是二叉树结构,各元素之间没有没有严格的的物理顺序关系,也就是说元素在容器中并没有保存元素置入容器时候的逻辑顺序而是在值中选择一个值作为关键字,这个关键字对值起到索引的作用,便于查找操作,Set,map,
容器类自动申请是释放内存,无需new和delete操作。
QT
信号和槽函数
**信号:**signals返回值是void,只需要声明,不需要重载,可以有参数,并且可以发生重载
**槽函数:**可以写在public slots下,在Qt5.0下可以写成全局函数或者lambda表达式中
当信号和槽发生重载的时候需要利用函数指针明确指出函数地址
Qstring 转为char* 的方法
通过.toUtf8转为QByteArray类型,通过.data()转为char*
lptr),mSize(0),mCapacity(0){
if(){
pArr = new cap;
mCapacity = cap;
}
}
~CDynamicArr(){
if(pArr){
delete []pArr;
pArr= nullptr;
mSize=mCapacity =0;
}
}
public:
//添加—尾添加效率高
void pushBack(const T & t){
if(mSize>=mCapacity){
//
}
else{
}
}
};
## STL
STL是(*standard template library*)中文译名为标准模板库,是C++的一部分,以源码的方式提供,体现了泛型编程的思想,大部分的算法被抽象,被泛化独立有与之对应的数据结构,用于相同或者相近的方式处理不同的情形,为开发者提供了一个可扩展的应用框架;一个显著特点就是数据结构和算法的分离;
STL包含六大组件:
- 容器(container)
- 迭代器(iterator)
- 算法(Algorithm)
- 容器适配器(Adapter)
- 空间分配器(Allcator)
- 仿函数(Function Objects)
### 容器(container)
各种数据结构,如vector,list,deque,set,map,array等,用来存放各种数据结构,从实现上来看,STL是一种类模板(class template)
容器主要分为两大类:
- 序列式容器:序列式容器强调值的排序,序列式容器中每个元素均有固定的位置,除非用删除或者插入的操作来改变位置,如vector,deque,list
- 关联式容器:关联式式容器,他是非线性的结构,更准确的说是二叉树结构,各元素之间没有没有严格的的物理顺序关系,也就是说元素在容器中并没有保存元素置入容器时候的逻辑顺序而是在值中选择一个值作为关键字,这个关键字对值起到索引的作用,便于查找操作,Set,map,
容器类自动申请是释放内存,无需new和delete操作。
## QT
### 信号和槽函数
**信号:**signals返回值是void,只需要声明,不需要重载,可以有参数,并且可以发生重载
**槽函数:**可以写在public slots下,在Qt5.0下可以写成全局函数或者lambda表达式中
当信号和槽发生重载的时候需要利用函数指针明确指出函数地址
>Qstring 转为char* 的方法
>
>通过.toUtf8转为QByteArray类型,通过.data()转为char*
>
>