该文章只是c++部分知识点,主要以重点为主,以后还会更新
面向对象编程的概念(掌握)
面向对象的是在学习过程中逐步形成的,主要在Qt的中会得到实现,面向对象有三大核心特点:
(记住)
封装 - >继承 -> 多态
面向对象还有两个最基础的概念:
类、对象
【例子】:如果想要把大象装到冰箱里,应该怎么办?
1》面向过程的编程思想:是以”怎么解决问题"为核心;
1.(我)打开冰箱门
2.(我)把大象装进去
3.(我)把冰箱门关上
面向过程的语言,它重点关注的是步骤(算法),可以认为”算法”是一系列有序的步骤,只需按照步骤去做,就可以得到预期的结果,所以我们面向过程的编程语言通常一系列有序的语句,这种方式更加符合我们计算机执行命令的本质。特点是开发效率低,运行的效率高。
2》面向对象的编程思想:是以”谁来解决问题“为核心。
1.(我)把大象和冰箱叫过来
2.(我)给给大象和冰箱分配任务
3. 大象和冰箱执行自行组装
面向对象的语言,重点关注如何“管理”,程序员在程序中管理“各种对象”,可以认为,面向对象的语言,关注的的数据之间的关系,这种方式更接近于人类的思考方式。特点是开发效率高,运行效率低。
C++兼容面向过程与面向对象,但是以面向对象为主。
C++语言的特点(掌握)
1>作为C的超集,C++继承了C的所有优点,于C 语言兼容,支持结构化的程序设计。
2>对C语言进行了一系列的扩充,比如数据类型、修复了C中一些漏洞,提供了更好的类型检查和编译时的分析。
3>生成目标程序的质量高,执行效率高。
4> 支持面向对象的程序设计,通过类和对象把数据(属性)和数据的操作(方法)封装在一起,模块的独立性更强。
5> 提供了异常处理机制,简化了程序的出错处理。
6>面向对象的三大特征:(抽象)、封装、继承、多态。
引用(重点)
1 概念及定义
可以把引用理解为给变量取别名(比如李四取小名叫小李子、四儿)。但底层的原理是常量指针(常量指针:;指针常量:;),与指针不同的是引用不会另外开辟内存空间,减少了内存的消耗。
定义格式:数据类型& 引用名 = 引用的目标;int & ra = a;//给变量a取别名叫ra;
要求:引用定义的同时必须进行初始化;引用的类型必须和引用的目标类型保持一致;引用的目标一旦确定就不能更改。
2 引用的基本使用
#include <iostream>
#include <string>
//字符串的头文件,C语言的头文件string.h,如:strcopy(),strlen(),strcmp()
//而C++的字符串的头文件代表的是一个类
using namespace std;
int main()//C++规定,主函数的返回值必须是int类型的
{
int a = 10;
int * pa = &a;
cout<<"&a"<<&a<<endl;
cout<<"&pa:"<<&pa<<endl;//会单独开辟一片内存空间的
cout<<"pa:"<<pa<<endl;
int b = 10;//初始化
int & rb = b;//给b变量取别名叫rb
cout<<"&b:"<<&b<<endl;//0x61fe80
cout<<"&rb:"<<&rb<<endl;//0x61fe80
cout<<"b:"<<b<<endl;//10
cout<<"rb:"<<rb<<endl;//10
//int & rb = a;error 重定义
rb = a;//这是表示给a取别名叫rb吗?
//不是,是赋值
cout<<"&rb:"<<&rb<<'\t'<<"&a:"<<&a<<endl;
return 0;
}
3 常引用
格式:const 数据类型& 引用名 = 引用的目标;
功能:不能通过引用来改变引用目标的值,但是可以通过引用的目标来改变值,通常可以将常引用用于函数的形参以保证形参不会被修改。
#include <iostream>
#include <string>
//字符串的头文件,C语言的头文件string.h,如:strcopy(),strlen(),strcmp()
//而C++的字符串的头文件代表的是一个类
using namespace std;
int add(const int& a,const int& b)//常引用,当我们的引用放在函数的形参位置时,可以不进行初始化
{
return a+b;//将引用设置为常引用时,函数体的内部不会对形参进行更改
}
int main()//C++规定,主函数的返回值必须是int类型的
{
int a = 10;
int & ra = a;
int b = 30;
const int & rb = b;
ra = 10;
//rb = 30;//error
b = 20;
cout<<"sum:"<<add(a,b)<<endl;
return 0;
}
4 结构体中的引用成员
#include <iostream>
#include <string>
//字符串的头文件,C语言的头文件string.h,如:strcopy(),strlen(),strcmp()
//而C++的字符串的头文件代表的是一个类
using namespace std;
struct Student
{
int h = 43;
int age;
int & score;//引用成为结构体成员时可以不进行初始化
int & height = h;//也可以在结构体中进行初始化操作
//Student(int h,int a,int& s,int& he):h(h),age(a),score(s),height(he){}
//这种初始化方式使用圆括号进行,需要写构造函数
};
int main()
{
int sco = 98;
int hei = 180;
//Student s1(8,9,sco,hei);//结构体中使用引用必须初始化
Student s2={8,9,sco,hei};
//1、如果结构体中对引用没有初始化,必须在定义结构体变量的时候进行初始化
//2、如果结构体中对引用初始化,可以在定义定义结构体变量的时候不进行初始化
//cout<<s1.h<<endl<<s1.score<<endl;
return 0;
}
5 指针和引用的区别(笔试面试题)
1、引用访问一个变量是直接访问,而指针是间接访问;
2、引用是一个变量的别名,不进行内存的分配;而指针变量需要分配内存空间;
3、引用的目标一旦确定就不能更改,而指针可以进行更改它的指向;
4、引用必须要进行初始化,而指针可以指向空,不指的话是一个野指针;
5、sizeof的含义不同:引用的结果为引用类型的大小,而指针的地址空间大小是固定的。
函数重载
通常情况下,我们需要几个函数来实现相似的功能:int sum(int a,int b);int sum1(int a,int b,int c);在C中需要定义两个不同的函数来实现了相似的功能;C++中可以允许定义名字相同的函数,但函数形参的类型或者个数不同才能实现函数重载。
注意:函数的重载与返回值类型无关
目的:是实现了一名多用
#include <iostream>
using namespace std;
int sum(int x,int y)
{
return x+y;
}
//double sum(int x,int y)//error,函数的重载与函数的返回值的类型无关
//{
// return x+y;
//}
double sum(double x,double y)
{
return x+y;
}
int sum(int x,int y,int z)
{
return x+y+z;
}
int main()
{
cout<<sum(1,4,5)<<endl;
cout<<sum(24.0,4.9)<<endl;
return 0;
}
函数的默认参数
给定形参一个默认值,如果实参传递值,使用的是实参的值,如果实参没有传递,则使用的是默认值。
如果调用具有默认参数的函数时,如果没有给实参,使用的是函数形参的默认值;如果给实参,使用的是函数实参;从左到右进行匹配,默认值遵循向右原则,当函数中某个参数设定为默认值后,其右边的所有参数都必须设定为默认值。
#include <iostream>
using namespace std;
int sum(int x = 0,int y = 0)//函数的默认参数
{
cout<<"3"<<endl;
return x+y;
}
int sum(int x=0,int y=0,int z = 0)
//参数的匹配按照从左到右的原则进行匹配
//形参的默认参数的右边必须是默认参数
{
cout<<"8"<<endl;
return x+y+z;
}
int main()
{
cout<<sum(24,5,8)<<endl;
// cout<<sum()<<endl;
// cout<<sum(2,5)<<endl;
//当我们使用函数默认参数以及函数重载时,应该避免两种情况同时出现
return 0;
}
哑元函数(熟悉)
一个函数的参数只有类型没有名字,则这个参数把它叫做哑元,这样函数叫做哑元函数。
用途:如果函数形参的参数不使用,可以使用哑元来消除警告
#include <iostream>
using namespace std;
void print(string)//哑元函数
{
cout<<"String"<<endl;
}
void print(int)
{
cout<<"Int"<<endl;
}
void print(double)
{
cout<<"Double"<<endl;
}
void print(char c)//如果不使用参数的话会给一个警告
{
cout<<"Char"<<endl;
}
int main()
{
print(4.5);
print(5);
print("hello");
return 0;
}
命名空间
C++中命名空间的最大意义在于避免不同文件之中标识符的冲突问题。
1std的用法(熟悉)
1>使用作用域限定符进行直接访问std空间里的成员;
2> using + 命名空间名::空间成员; //具体导入
3> using namespace 命名空间名; //全部导入
#include <iostream>
using std::cout;//具体导入,说明cout是属于std标准命名空间,此行之后遇到的所有cout都默认是std命名空间里面的
using namespace std;//进行全部导入,此行之后,所有标准命名空间的成员将不再进行std声明
int main()
{
//::作用域限定符
std::cout<<"hello world"<<std::endl;//第一种写法
cout<<"hello world"<<std::endl;//第二种写法
cout<<"hello world"<<endl;//第三种写法
return 0;
}
2 自定义命名空间
关键字:namespace
命名空间里包含的是:变量与函数,与C语言是不同的
#include <iostream>
using namespace std;
//using T1::a;//error,命名空间的声明需在自定义命名空间的定义下面进行声明
namespace T1//自定义命名空间T1
{
int a = 0;
string name = "zhangsan";
void show()
{
cout<<"a:"<<a<<endl<<"name:"<<name<<endl;
}
};
using T1::a;//具体导入
using namespace T1;//全部导入
int main()
{
/*第一种用法---->直接声明*/
T1::a = 12;
T1::name = "lisi";
T1::show();
/*第二种用法--->具体导入*/
a = 23;
T1::show();
/*第三种用法--->全部导入*/
a = 43;
name = "wang";
show();
cout << "Hello World!" << endl;
return 0;
}
3 冲突问题
1> 不同命名空间中含有相同成员冲突,使用作用域限定符进行区分;
2>全局变量和命名空间中的成员冲突时:如果不加using namespace声明时默认访问全局变量;如果加了using namespace,访问全局变量时需要使用匿名空间的访问方法(加了作用域限定符在全局变量之前)
3>局部变量和命名空间中成员冲突时:如果不加using namespace时默认访问的时局部变量,如果加了using namespace也不冲突,因为优先使用局部变量,根据的是就近原则。
#include <iostream>
using namespace std;
//using T1::a;//error,命名空间的声明需在自定义命名空间的定义下面进行声明
namespace T1//自定义命名空间T1
{
int a = 0;
string name = "zhangsan";
void show()
{
cout<<"a:"<<a<<endl<<"name:"<<name<<endl;
}
};
namespace T2
{
int age;
string name;
void show()
{
cout<<"a:"<<age<<endl<<"name:"<<name<<endl;
}
};
using namespace T2;//此行代表将T1空间里的所有成员进行提前声明
using namespace T1;
string name;//全局变量
int main()
{
::name = "lisi";//right,默认使用的是全局变量,
//如果加了using namespace之后,非要访问全局变量的话这种方法是使用的匿名空间
int age = 10;//定义局部变量
age = 34;
//如果没有使用using namespace时,默认使用的是局部变量
//如果加了using namespace时,也是使用的是局部变量,根据就近原则访问
cout << "Hello World!" << endl;
return 0;
}
4 嵌套使用(了解)
#include <iostream>
using namespace std;
//using T1::a;//error,命名空间的声明需在自定义命名空间的定义下面进行声明
namespace T1//自定义命名空间T1
{
int a = 0;
string name = "zhangsan";
void show()
{
cout<<"a:"<<a<<endl<<"name:"<<name<<endl;
}
namespace T2
{
int age;
string name;
void show()
{
cout<<"a:"<<age<<endl<<"name:"<<name<<endl;
}
};
};
using namespace T1::T2;//此行代表将T1空间里的所有成员进行提前声明
using namespace T1;
string name;//全局变量
int main()
{
T1::T2::show();
T1::show();
cout << "Hello World!" << endl;
return 0;
}
5 两个命名空间相同时(了解)
可以同时存在两个相同的命名空间名,但是在两个命名空间里面成员(变量或函数)不能相同。
#include <iostream>
using namespace std;
//using T1::a;//error,命名空间的声明需在自定义命名空间的定义下面进行声明
namespace T1//自定义命名空间T1
{
int a = 0;
string name = "zhangsan";
void show()
{
cout<<"a:"<<a<<endl<<"name:"<<name<<endl;
}
};
namespace T1
{
int age;
string name1;
void show1()
{
cout<<"a:"<<age<<endl<<"name:"<<name<<endl;
}
};
using namespace T1;
string name;//全局变量
int main()
{
T1::show();
cout << "Hello World!" << endl;
return 0;
}
new/delete与malloc/free的区别(笔试面试题)
1> malloc/free是C中的库函数,new/delete是C++中的关键字;
2>new在申请空间的同时可以进行初始化,而malloc不可以;
3>new与malloc申请空间若没有初始化默认为随机值;
4>new申请空间的时候,会按照类型自动计算分配空间的大小,而malloc需要手动用sizeof()计算;
5>new 申请空间时需要什么类型就直接返回什么类型的空间,而malloc申请时默认返回的是(void *),需要强制转换为自己需要的类型;
6>new申请空间空间时,会调用构造函数,而malloc不会;
7>delete释放空间时,会调用析构函数,而free不会。
类和对象
类:是对象的抽象
对象:是类的实例化,万物皆可对象
属性:对象的特点
方法:对象会做事情,也就是函数
explicit关键字(了解)
只对构造函数有用,用来避免隐式类型转换。
#include <iostream>
using namespace std;
class Cuboid
{
public:
explicit Cuboid(int l,int w,int h)//有参构造函数
{
length = l;
width = w;
high = h;
cout<<"has para consturctor function"<<endl;
}
Cuboid()//无参构造函数
{
cout<<"no para consturctor function"<<endl;
}
private:
int length,width,high;
};
int main()
{
Cuboid c1(1,2,4);//编译器这样认为认为:c1.Cuboid(1,2,4)
Cuboid c2;//c2.Cuboid();
//Cuboid c3 = {1,4,5};//error内部进行了隐式类型转换
return 0;
}
构造函数的初始化列表(掌握)
用另外一种形式来表示构造函数
explicit Cuboid(int l,int w,int h):length(l),width(w),high(h)
{
//构造函数的初始化列表形式
cout<<"has para consturctor function"<<endl;
}
使用参数表的场景(掌握)
1> 形参的名字和类中属性的名字相同的时候
2>当属性是引用的时候必须使用初始化列表的方式来进行初始化
3>当数据成员是const类型的时候
#include <iostream>
using namespace std;
class Cuboid
{
public:
// explicit Cuboid(int l,int w,int h)//有参构造函数
// {
// length = l;
// width = w; //警告!!!
// high = h;
// cout<<"has para consturctor function"<<endl;
// }
Cuboid(int length,int width,int h):length(length),width(width),high(h){}
// Cuboid()//无参构造函数
// //当成员有引用的时候,必须在定义对象的同时给它初始化
// //如果在这里写了一个无参构造,暗含我不想给对象进行初始化,违背了引用的使用特点
// {
// cout<<"no para consturctor function"<<endl;
// }
void show()
{
cout<<length<<","<<width<<","<<high<<endl;
}
private:
int length;
const int width;
int &high;
};
int main()
{
Cuboid c1(1,2,4);//编译器这样认为认为:c1.Cuboid(1,2,4)
//Cuboid c2;//c2.Cuboid();
//Cuboid c3 = {1,4,5};//error内部进行了隐式类型转换
return 0;
}
4> 数据成员中有另一个类的实例化的对象的时候
#include <iostream>
using namespace std;
class Student
{
int age;
string name;
public:
Student(int a,string n):age(a),name(n){}
};
class Teacher
{
int score;
Student s1;
public:
Teacher(int s,int a,string n):score(s),s1(a,n){}
//s1(a,n)调用的是Student类中的构造函数
};
int main()
{
Teacher t1(32,43,"dfd");
return 0;
}
this指针(掌握)
1 为什么需要this指针
成员函数不占类的内存空间,当对象调用成员函数的时候,不同对象调用的同一个成员函数,为什么不同对象在调用同一个成员函数的时候数据不会被混?
分析下来,在成员函数里有一个隐藏指针;
#include <iostream>
using namespace std;
class Student
{
int age;
string name;
public:
Student(int age,string name)//构造函数
{
this->age = age;
this->name = name;
}
void show()//成员函数不占类的内存空间
//当对象调用成员函数的时候,不同对象调用的同一个成员函数
//为什么不同对象在调用同一个成员函数的时候数据不会被混?
//show(Student * const this)
{
cout<<this->age<<","<<this->name<<endl;
}
};
int main()
{
Student s1(24,"zhang");
s1.show();//24,zhang,编译器:s1.show(&s1)
Student s2(342,"zhao");
s2.show();//342,zhao
return 0;
}
//s1.show(),编译器会处理为s1.show(&s1)
//void show(Student * const this);
2 使用this指针的场景
1> 数据成员和形参名相同时使用this来访问数据成员加以区分
2> 返回自身的引用时
类内声明,类外定义(重点)
#include <iostream>
using namespace std;
class Student
{
int age;
string name;
public:
Student(int age,string name);//类内声明
Student();
~Student();
void show();
};
Student::Student(int a,string n)//类外定义
{
age = a;
name = n;
}
Student::Student()
{
}
Student::~Student()
{
}
void Student::show()//普通函数的类外定义
{
cout<<"name:"<<name<<'\t'<<"age:"<<age<<endl;
}
int main()
{
Student s1(2,"zhag");//s1.Student(&s1);
Student s2;
Student s3(53,"zhang");
return 0;
}
深拷贝和浅拷贝(重点)
类中的属性有指针的时候会出现深浅拷贝的情况。
深拷贝和浅拷贝的区别:浅拷贝主要是对指针的拷贝,使两个指针指向同一片内存空间;深拷贝是为了解决浅拷贝的问题重新开辟一片内存空间,从而使两个指针变量指向了不同的空间,因此在析构的时候不会出现double free的现象。
默认的拷贝构造函数属于浅拷贝。
#include <iostream>
using namespace std;
class Student
{
string name;
int * age;
public:
//Student(string n,int a):name(n),age(new int(a)){}
Student(string n,int a)
{
name = n;
age = new int(a);
cout<<"constructor"<<endl;
}
// Student(const Student& O)//浅拷贝
// {
// this->name = O.name;
// this->age = O.age;
// cout<<"copy constructor"<<endl;
// }
Student(const Student& O)//深拷贝
{
this->name = O.name;
this->age = new int(*(O.age));
}
~Student()
{
delete age;
}
};
int main()
{
Student s1("zhang",18);
Student s2(s1);
return 0;
}
default关键字
作用:获取编译器自动生成的默认特殊成员函数的高的代码执行效率。
来源:如果有了用户自定义的构造函数,编译器不会为它生成隐式的默认构造函数,如果需要用到默认构造函数来创建对象时候,需要自己显式的定义默认构造函数,工作量加大,此外,手动编写的默认构造函数的代码执行效率比编译器自动生成的默认构造函数低。
解决方式:C++11引入了新的特性:default函数,程序员只需在函数函数声明后加上=default关键字;就将该函数声明为default函数,编译器将为显式声明default函数自动生成函数体,如Student::Student(){ },该函数可以比我们自定义的默认构造函数有更高的代码的执行效率。
注意:default函数仅适用于特殊的成员函数,且该特殊成员函数没有默认参数。default函数既可以在类内定义也可以在类内声明类外定义。
#include <iostream>
using namespace std;
class Student
{
string name;
int * age;
public:
Student();
~Student()
{
delete age;
}
};
Student::Student()=default;
int main()
{
return 0;
}
static关键字(重点)
1 静态成员变量
1>只能在类内定义类外初始化;
2>不初始化时默认值为0;
3>可以通过类名或者对象名来调用;
4>即使不定义对象也为静态成员变量分配空间,因此静态成员变量不属于某个对象的,而是对象共用一个。
2 静态成员函数
1>静态成员函数只能访问静态成员变量,不能访问非静态成员变量;
2>可通过类名或者对象名来进行访问;
3> 不属于某个对象,而是属于整个类的,因此静态成员函数没有this指针。
3 静态局部变量
1>限制全局变量的作用域;
2>延长局部变量的生命周期;
注意:不建议大量使用静态修饰,生命周期太长,内存占有率较高;不符合面向对象的特点。
#include <iostream>
using namespace std;
class Student
{
int score;
public:
Student()=default;
~Student()=default;
Student(int s):score(s){}
static void show()
{
cout<<'\t'<<"age:"<<age<<endl;
}
static int age;
};
void fun1()
{
static int a = 1;//静态局部变量
}
int Student::age=10;
int main()
{
Student s1(43);
s1.show();
Student s2(32);
Student::show();
//Student::age = 42;可通过类名来进行访问静态成员变量
//s2.age;//也可通过对象名来访问
//Student::score = 324;error,不属于静态成员变量
//cout<<"sizeof:"<<sizeof(s1)<<endl;
return 0;
}
const关键字(重点)
1 常成员变量
1>定义格式:const 数据类型 变量名;
2>注意:常成员变量只能通过构造函数的初始化列表形式;
2 常成员函数
1>定义格式:返回类型 函数名(参数...) const { }
2>注意:
1)只能引用本类中成员变量,而不能对其进行修改,无论成员变量是否是const修饰的;
2)常成员函数和普通成员成员函数同名时可发生重载;
3)常成员函数this指针的本来面目:const 类名 * const this;
3 常对象
1>定义格式:const 类名 对象名 | 类名 const 对象名
2>注意:
1)常对象只能调用常成员函数;
2)普通对象优先调用普通成员函数,如果没有普通成员函数,则调用常成员函数;
#include <iostream>
using namespace std;
class Student
{
const int score;//常成员变量
int age;
public:
Student()=default;//警告:常成员变量必须初始化,给了无参构造暗含在定义对象的时候不初始化;
~Student()=default;
Student(int s,int a):score(s),age(a){}
void show()const//常成员函数,show(const Student * const this);
{
//age = 42;error
cout<<'\t'<<"age:"<<age<<endl;
}
void show()//show(Student * const this);
{
cout<<"score:"<<score<<endl;
}
};
int main()
{
const Student s1(43,2);
s1.show();
Student s2(24,5);
s2.show();
return 0;
}
全局函数作为友元函数(掌握)
全局函数本来不可以访问类中的私有成员的,当我们在类中对全局函数进行友元声明(通过关键字:friend)的时候,就可以访问到类中的私有成员。
#include <iostream>
class Student
{
std::string name;
int age;
public:
Student(std::string n,int a):name(n),age(a){}
friend void show(Student&);//友元声明全局函数
//通过此行全局就可以访问到类中私有成员了
};
void show(Student& s1)
{
std::cout<<"name:"<<s1.name<<'\t'<<"age:"<<s1.age<<std::endl;
}
int main()
{
Student s1("zhang",78);
show(s1);
return 0;
}
类的成员函数作为友元函数(熟悉)
用一个类中的成员函数作为另一个类中的友元函数。
#include <iostream>
class Date;//类的前置声明(只是声明,告诉Time类后面有一个Date类,不知道Date类中有什么成员)
class Time
{
public:
Time(int h,int m,int s):hour(h),minute(m),seconds(s){}
void show(const Date & O);//此函数需要在类外定义
//Time类中有Date类的声明,声明是不知道类中的属性的
//所以如果在类内访问Date类中的成员的时候是不知道有哪些成员的因此是访问不到的
//解决方法:需在Date类的定义后面进行访问Date类的数据成员
private:
int hour,minute,seconds;
};
class Date
{
public:
Date(int y,int m,int d):year(y),month(m),day(d){}
friend void Time::show(const Date &);//对Time类中的成员函数进行友元声明后,可以访问到Date类中的私有成员
private:
int year,month,day;
};
void Time::show(const Date &O)
{
std::cout<<hour<<","<<minute<<","<<seconds<<std::endl;
std::cout<<O.year<<"/"<<O.month<<"/"<<O.day<<std::endl;
}
int main()
{
Time t1(9,39,32);
Date d1(2023,7,10);
t1.show(d1);
return 0;
}
继承中的特殊成员函数(掌握)
1 构造函数
1>子类不会继承父类的构造函数;
2>为了完成在子类中完成父类的成员的初始化,需要在子类的初始化列表中显性的调用父类的构造函数;
3>构造函数的调用顺序是先调用父类的构造函数再调用子类的构造函数。
2 析构函数
1>子类不会继承父类的析构函数;
2>不管子类是否显性调用父类的析构函数,父类的析构函数都会被自动调用来完成子类的资源释放;
3>调用顺序:先调用子类的析构函数,再调用父类的析构函数;
3 拷贝构造函数
1>子类不会继承父类的拷贝构造函数;
2>子类中如果显性的定义了拷贝构造函数,需要在子类的拷贝构造函数的初始化列表中显性调用父类的拷贝构造函数;如果子类没有定义,则使用默认的拷贝构造函数;
4 拷贝赋值函数
1>子类不会继承父类的拷贝赋值函数;
2>如果子类中显性的定义了拷贝赋值函数,需要在子类的拷贝赋值函数的函数体中显性调用父类的拷贝赋值函数,注意需要加作用域限定符;如果子类没有定义拷贝赋值,使用默认的拷贝赋值函数。
#include<iostream>
using namespace std;
class Person
{
protected:
string name;
int age;
public:
Person(string n,int a):name(n),age(a){}
Person(const Person& O):name(O.name),age(O.age)//构造函数的初始化列表方式
{
cout<<"Person:: copy constructor"<<endl;
}
Person& operator=(const Person& O)//拷贝赋值函数
{
this->name = O.name;
this->age = O.age;
cout<<"Person:: copy assign"<<endl;
return *this;
}
Person()=default;
~Person()
{
cout<<"Person::destructor"<<endl;
}
};
class Student:public Person
{
protected:
int score;
public:
Student(int s,string n,int a):score(s),Person(n,a){}
Student(Student& O):score(O.score),Person(O)
//Student(Student& O):score(O.score),Person(O.name,O.age)
/*
这里需要直接调用父类的拷贝构造函数,形式是Person(const Person& O),传的Person(O.name,O.age)不行
需要这样操作Person(O)
*/
{
cout<<"Student:: copy constructor"<<endl;
}
//调用直接父类的构造函数,与基类中的构造无关
Student& operator=(const Student& O)
{
this->score = O.score;
Person::operator=(O);
cout<<"Student:: copy assign"<<endl;
return *this;
}
Student()=default;
~Student()
{
cout<<"Student::destructor"<<endl;
}
};
int main()
{
Student s1(32,"zhang",24);
Student s2(s1);
Student s3;
s3 = s1;
//如果不自定义拷贝构造函数,会使用子类默认的拷贝构造函数不会出错
return 0;
}
虚继承(掌握)
1 菱形继承问题(掌握)
菱形继承在汇聚子类中会出现右中间子类造成的数据冗余问题;
2 虚继承
为了解决以上麻烦,需要通过虚继承的方式,就是在中间子类的继承方式前加一个关键字virtual。
虚继承的作用是在菱形继承中当基类通过多条路径被一个派生类继承的时候,该派生类只保留该基类中的成员一次。
#include <iostream>
using namespace std;
class A
{
public:
A()=default;
A(int a):v_a(a){}
void show()
{
cout<<"A::v_a:"<<v_a<<endl;
}
protected:
int v_a;
};
class B:virtual public A
{
public:
B()=default;
B(int a,int b):A(a),v_b(b){}
void show()
{
cout<<"B::v_a:"<<v_a<<'\t'<<"B::v_b:"<<v_b<<endl;
}
protected:
int v_b;
};
class C:virtual public A
{
public:
C()=default;
C(int a,int c):A(a),v_c(c){}
void show()
{
cout<<"C::v_a:"<<v_a<<'\t'<<"C::v_c:"<<v_c<<endl;
}
protected:
int v_c;
};
class D:public B,public C
{
public:
D(int a,int b,int c,int d):B(a,b),C(a,c),v_d(d){}
void show()
{
cout<<"D::v_d:"<<v_d<<'\t'<<"D::B::v_b:"<<v_b<<'\t'<<"D::C::v_c:"<<v_c<<'\t'<<
"D::B::v_a:"<<B::v_a<<'\t'<<"D::C::v_a:"<<C::v_a<<endl;
//以上是不加virtual关键字的时候需要这样处理
cout<<"D::v_d"<<v_d<<'\t'<<"D::B::v_b:"<<v_b<<'\t'<<"D::C::v_c:"<<v_c<<'\t'<<"v_a:"<<
v_a<<endl;
//加了虚继承后只保留了父类B和C中成员的一份
}
private:
int v_d;
};
int main()
{
D d1(24,5,342,2);
d1.show();
return 0;
}
实现多态的必要条件(掌握)
1> 有继承关系
2> 基类中有虚函数,且子类需要重写虚函数
类型:virtual 返回类型 函数名(){}
3> 基类的指针或者引用,指向子类的对象,就会形成动态多态。
#include<iostream>
using namespace std;
class Parent
{
public:
Parent(int a):age(a){}
//虚函数,可以解决用同一个调用形式,能通过父类指针或引用来调用派生类的函数又能调用基类的同名函数。
virtual void show()
{
cout<<"Parent::age:"<<age<<endl;
}
protected:
int age;
};
class Son : public Parent
{
public:
Son(int a,int s):Parent(a),score(s){}
void show()
{
cout<<"Son::age:"<<age<<'\t'<<"Son::score:"<<score<<endl;
}
private:
int score;
};
int main()
{
Parent p1(23);//定义父类对象
Son s1(13,43);//定义子类对象
Parent * pointer = NULL;//定义父类指针
pointer = &s1;//父类指针指向子类对象
pointer->show();//通过父类指针进行成员调用;
//若不加virtual关键字给函数时,输出的时候调用的是父类继承下来的成员函数
//但是我想要的是通过父类指针来调用子类的成员函数,那么就需要将父类的成员函数设置为虚函数
pointer = &p1;//改变父类指针的指向
pointer->show();//同样可以调用,此时调用的是父类中的成员函数
Parent& rp1 = s1;//父类引用指向子类对象
rp1.show();//通过父类引用调用子类成员
Parent& rp2 = p1;//父类引用指向父类对象
rp2.show();//通过父类引用调用父类成员
return 0;
}
虚析构函数(掌握)
作用:当基类指针指向子类对象的时候,需要把基类的析构函数设置为虚析构函数,防止内存泄漏(基类析构函数不定义为虚析构时,当我们用基类指针指向子类对象的时候,只会调用基类析构函数);
四大函数中只能将析构函数设置为虚析构,其它都不能设置;
如果将基类的析构函数虚析构后,那么由该基类派生出的所有子类的析构函数都是虚析构函数。
#include<iostream>
using namespace std;
class BaseClass
{
public:
BaseClass() {cout<<"Base::constructor"<<endl;}
virtual ~BaseClass()
{
cout << "delete BaseClass" << endl;
}
};
class ChildClassA : public BaseClass
{
public:
ChildClassA() {cout<<"A::constructor"<<endl;}
~ChildClassA()
{
cout << "ChildClassA Delete" << endl;
}
};
class ChildClassB : public ChildClassA
{
public:
ChildClassB() {cout<<"B::constructor"<<endl;}
~ChildClassB()
{
cout << "ChildClassB Delete" << endl;
}
};
int main()
{
BaseClass * pa = new ChildClassA();
cout<<"--------------------------"<<endl;
BaseClass * pb = new ChildClassB();
cout<<"--------------------------"<<endl;
ChildClassA * pc = new ChildClassB();
cout<<"--------------------------"<<endl;
delete pa;
cout<<"--------------------------"<<endl;
delete pb;
cout<<"--------------------------"<<endl;
delete pc;
cout<<"--------------------------"<<endl;
return 0;
}
/*基类析构函数不定义为虚析构时,当我们用基类指针指向子类对象的时候,只会调用基类析构函数*/
//Base::constructor
//A::constructor
//--------------------------
//Base::constructor
//A::constructor
//B::constructor
//--------------------------
//Base::constructor
//A::constructor
//B::constructor
//--------------------------
//delete BaseClass
//--------------------------
//delete BaseClass
//--------------------------
//ChildClassA Delete
//delete BaseClass
//--------------------------
/*****************************设置为虚析构函数后*********************************/
/*当基类设置为虚析构,子类的析构函数会被调用,且子类的析构函数不需要设置为虚析构,孙子类的析构函数仍然会调用*/
//Base::constructor
//A::constructor
//--------------------------
//Base::constructor
//A::constructor
//B::constructor
//--------------------------
//Base::constructor
//A::constructor
//B::constructor
//--------------------------
//ChildClassA Delete
//delete BaseClass
//--------------------------
//ChildClassB Delete
//ChildClassA Delete
//delete BaseClass
//--------------------------
//ChildClassB Delete
//ChildClassA Delete
//delete BaseClass
//--------------------------
纯虚函数与抽象类(掌握)
这个虚函数的函数名只作为一种功能的描述,并没有具体的实现,我们把这种函数叫做纯虚函数,它也叫做接口函数,或接口。
包含有这样纯虚函数的类叫做抽象类。在这个抽象类中我们一般不去定义任务属性。抽象类主要用来作为最顶层的基类使用。也可以说是定义接口使用的,所以说抽象类也叫接口类。
virtual 返回值 函数名(形参...) = 0;
所以说抽象类的意义不在于类的实现,而是顶层类的设计;
使用抽象类作为基类,在派生类中对抽象类中的定义的纯虚函数进行重写;如果派生类没有重写父类的纯虚函数,那么这个派生类也是一个抽象类。
抽象类不可以定义对象,可以使用抽象类的指针或者引用来指向子类对象,此时可以调用在子类中重写的虚函数。
#include <iostream>
using namespace std;
class A
{
public:
virtual void show() = 0;//纯虚函数
virtual ~A() = default;//虚析构函数
};
class B:public A
{
public:
void show();
private:
int age;
};
int main()
{
//A a;error,抽象类不可以定义对象
A* p = new B;
p->show();//可通过父类指针访问子类成员
return 0;
}
void B::show()
{
cout<<"age:"<<age<<endl;
}
STL六大组件(记住)
STL大体分为六大组件,分别是:容器、空间配置器、适配器(配接器)、算法、迭代器、仿函数。
1.容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据
2.算法:各种常用的算法,如sort、find、copy、for_each等
3.迭代器:扮演了容器和算法之间的胶合剂
4.仿函数:行为类似函数,可作为算法的某种策略
5.适配器:一种用来修饰容器或者仿函数或迭代器接口的东西
6.空间适配器:负责空间的配置和管理