一、类和对象
c++面向对象的三大特性:封装、继承、多态。
1.1、访问权限
公共权限 | public |
---|---|
保护权限 | protected |
私有权限 | private |
公共权限 | public | 类内可以访问 | 类外也可以访问 |
---|---|---|---|
保护权限 | protected | 类内可以访问 | 类外不可以访问 |
私有权限 | private | 类内可以访问 | 类外不可以访问 |
c++中struct和class 唯一的区别,在于默认的访问的权限不同。
区别:
- struct默认权限为公共
- class默认权限为私有
1.2、构造函数和析构函数
构造函数语法: 类名 () {}
- 构造函数,没有返回值也不写void;
- 函数名称与类名相同
- 构造函数可以有参数,因此可以发生重载;
- 程序再调用对象时,会自动调用构造,无需手动调用,而且只会调用一次。
析构函数语法:~类名(){}
- 析构函数,没有返回值也不写void;
- 函数名称与类名相同,在名称前加上符号**~**;
- 析构函数不可以有参数,因此也不会发生重载;
- 程序在对象销毁前会在动调用析构,无需手动调用,而且只会调用一次。
c++中拷贝构造函数调用的时机:
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
1.3、构造函数的调用规则
默认情况下:c++编译器至少会给一个类添加3个函数
- 默认构造函数(无参,函数体为空) (空实现)
- 默认析构函数(无参,函数体为空) (空实现)
- 默认拷贝构造函数,对属性进行值拷贝 (值拷贝)
构造函数调用规则如下:
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造;
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数。
总结:
如果我提供的是有参构造函数,编译器不会提供无参构造函数,但会提供拷贝构造函数;
如果提供的是拷贝构造函数,编译器不会提供默认构造和有参构造。
1.4、深拷贝和浅拷贝
浅拷贝: 简单的赋值拷贝操作;编译器提供的简单的拷贝构造函数都叫浅拷贝。(就是一个等号赋值操作)
深拷贝: 在堆区重新申请内存,进行拷贝操作。
class person
{
public:
person()
{
cout<<“person的默认构造函数调用”<<endl;
}
person(int age,int Height)
{
m_Age = age;
m_Height = new int(height); //在堆区开辟数据,new 一个int,返回的是 int*;
//注意:堆区开辟的数据,有程序员手动开辟,手动释放,在对象销毁前进行释放。比如:test01执行结束后,p1和p2就会销毁。
cout<<“person的有参构造函数调用”<<endl;
}
//自己实现拷贝构造函数,解决浅拷贝带来的问题。
person(const person& p)
{
cout<<“person拷贝构造函数的调用”<<endl;
//浅拷贝
//m_Age=p.m_Age;
//m_Height=p.m_Height; 这两行代码是编译器自己提供的(默认的浅拷贝)。
//深拷贝:我们需要重新在堆区创建一个内存。
m_Height=new int (*p.m_Height); //首先解引用这块数据,再在堆区重新开辟一个空间。
}
~person()
{
//析构代码:将堆区开辟的数据做释放操作
if(m_Height!=NULL)
{
delete m_Height;
m_Height=NULL; //为了防止野指针的出现
}
cout<<“person的析构函数调用”<<endl;
}
int m_Age;
int *Height; //定义一个指针,Height是指针,*只是用来定义它的,指向的是身高。为什么要用要定义身高指针,因为我们要把身高的数据开辟到堆区。
};
此时在利用系统的浅拷贝时,主要问题出在身高上(身高是指针),浅拷贝会直接拷贝指针过去,指针是地址,在进行地址释放时,会出现指针指向的堆区区域(就是我们之前开辟的)重复释放,会报错。
首先我们要知道,p1和p2哪个x先被释放,栈:先进后出 ,所以是p2会先被释放,p2和p1都会执行析构函数中的释放代码。
解决方法: 通过深拷贝进行解决。编译器提供的浅拷贝不好使,采用深拷贝,在开辟一个堆区 。
总结: 如果属性有在堆区开辟的,一定要自己提供拷贝构造函数,放置拷贝构造带来的问题。
1.5、 静态成员
- 静态成员变量属于整个类所有
- 静态成员变量的生命周期不依赖任何对象,为程序的生命周期
- 可以通过类名访问公有静态成员变量
- 所有对象共享类的静态成员变量可以通过对象名访问公有静态成员变量
- 静态成员变量需要在类外单独分配空间
- 静态成员变量在程序内部位于全局区
静态成员:在成员变量和成员函数前加上关键字static,称为静态成员。
1.5.1、静态成员变量
- 所有对象共享一份数据 A=100,B将其改为200时,A也会变为200
- 在编译阶段分配内存 //并不是在创建对象时,就将其分配在栈上或者堆上,代码运行前分为代码区和全局区,它分在了全局区。
- 类内声明,类外初始化
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
class person
{
public:
static int m_A;
//1、所有对象都共享同一份数据
//2、编译阶段就分配内存
//3、类内声明,类外初始化
//静态成员变量也是有访问权限的
private:
static int m_B; //类内声明,一定要类外初始化一下。
};
int person::m_A = 100;
void test01()
{
person p;
cout << p.m_A << endl; //此时打印是100;
person p2;
p2.m_A = 200;
cout << p.m_A << endl; //此时打印是200;静态成员变量共享同一份数据。
cout << p.m_B << endl; //此时会报错,私有权限,不可以访问
}
void test02()
{
//静态成员变量 不属于某个对象上,所有对象都共享同一份数据
//1、通过对象进行访问
person p;
cout << p.m_A <<endl;
//2、(也可以不创建对象,因为他本身就不属于哪个对象,是大家共享的),
通过类名直接访问:
cout << person::m_A << endl;
}
int main(){
system("pause");
return 0;
}
1.5.2、静态成员函数
在成员函数之前加上一个static,使他变成一个静态成员函数;
特点:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
class person
{
public:
//静态成员函数
static void func()
{
m_A = 100; //静态成员函数可以访问 静态成员变量
m_B = 200; //静态成员函数不可以访问 非静态成员变量,无法区分到底是哪个对象的m_B属性。
cout<<“static void func调用”<<endl;
}
static int m_A; //静态成员变量
int m_B; //非静态成员变量
//静态成员函数也得有访问权限的
private:
static void func2()
{
cout<<“static void func2调用”<<endl;
}
};
void test01()
{
//1、通过对象进行访问
person p;
p.func();
//2、通过类名进行访问
person::func();
person::func2(); //是肯定会报错的。私有权限,类外是不可以及进行访问的。
}
1.6、c++对象模型
在类中,所有的变量和函数,我们都称其为成员;
在c++中,类内的成员函数和成员变量分开存储
注意:
- 在c++中,类内成员变量和成员函数分开存储;
- 只有非静态成员变量才属于类的对象上,静态成员、静态成员函数、非静态成员函数均不属于类的对象上。
问: 空类占用的内存大小是多少?
class person
{
int m_A; //非静态成员变量,属于类的对象上。
static int m_B; //静态成员变量 不属于类对象。
void func(){}; //非静态成员函数 不属于类对象。
static void func2(){} ; //静态成员函数 不属于某一个对象上。
};
int person::m_B = 0;
void test01()
{
person p;
//空对象占用的内存为:1个字节;
//c++会给每个空对象分配一个字节的空间,是为了区分空对象占内存的位置;
//每个空对象也应该有一个独一无二的内存地址。
cout<<“size of p=”<<sizeof(p)<<endl;
//当加入一个成员变量时,此时打印内存大小,大小为4
}
1.7、this指针
c++中成员变量和函数是分开存储的;
每一个非静态成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用同一块代码。
问题:这一块代码怎么区分哪个对象在调用自己?
利用this指针及进行区分,this指针指向被调用的成员函数**所属的对象 **。
- p1调用成员函数,this就会指向p1;
- p2调用成员函数,this就会指向p2;
- …
this指针的用途:
- 当形参和成员变量同名时,可用this指针及进行区分;
- 在类的非静态成员函数中返回对象本身,可使用return *this。
1.8、友元
定义:
- 在程序里,有些私有属性,也想让类外特殊的一些函数或者类进行访问,需要用到友元技术。
- 友元的目的就是让一个函数或者类访问另一个类中的私有成员。
友元的关键字 :friend
友元的三种实现:
- 全局函数做友元
- 类做友元
- 成员函数做友元
//全局函数做友元
class Building
{
//告诉编译器 goodgay全局函数是Building类的好朋友,可以访问类中的私有内容。
friend void goodgay(Building*building);
public:
Building()
{
this->m_sittingroom=“客厅”;
this->m_bedroom=“卧室"; //对成员属性进行初始化
}
public:
string my_sittingroom;
private:
string my_bedroom;
};
//全局函数
void goodgay(Building*building)
{
cout << "好基友正在访问: " << building->m_SittingRoom << endl;
cout << "好基友正在访问: " << building->m_BedRoom << endl;
}
//全局函数test01
void test01()
{
Building b;
goodgay(&b);
}
int main()
{
test01();
system("pause");
return 0;
}
//类做友元
class Building;
class goodgay
{
public:
goodgay();
void visit();
private:
Building *building; //私有权限保护一个Building类的指针
};
class Building
{
//告诉编译器goodgay类是Building类的好朋友,可以访问building类中的私有内容
friend class goodgay;
public:
Building();
public:
string m_sittingroom; //客厅
private:
string m_Bedroom; //卧室
};
Building::Building() //Building类中的构造函数
{
this->m_SittingRoom = "客厅";
this->m_BedRoom = "卧室";
}
goodgay::goodgay() //goodgay中的构造函数
{
building = new Building;
}
void goodGay::visit()
{
cout << "好基友正在访问" << building->m_SittingRoom << endl;
cout << "好基友正在访问" << building->m_BedRoom << endl;
}
void test01()
{
goodgay gg;
gg.visit();
}
int main()
{
test01();
system(“pause”);
return 0;
}
1.9、继承
1.9.1、继承定义
class 子类:继承方式 父类 {}
继承方式:
- public,可以被任意实体访问;
- protected,只允许本类或子类的成员函数进行访问;
- private,只允许本类的成员函数访问;
注意:
- 继承时,所有的成员都会继承下来,只是能不能访问的问题;
- 父类中的私有内容:子类不管哪种方式都无法访问;
1.9.2、继承同名成员处理方式
- 访问子类同名成员,直接访问即可
- 访问父类同名成员 需要加作用域
class Base {
public:
Base()
{
m_A = 100;
}
void func()
{
cout << "Base - func()调用" << endl;
}
void func(int a)
{
cout << "Base - func(int a)调用" << endl;
}
public:
int m_A;
};
class Son : public Base {
public:
Son()
{
m_A = 200;
}
//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
void func()
{
cout << "Son - func()调用" << endl;
}
public:
int m_A;
};
void test01()
{
Son s;
//同名成员属性的处理方式
cout << "Son下的m_A = " << s.m_A << endl; //如果出现同名,直接访问,是子类中的
//如果通过子类对象 访问父类中的同名成员,需要加作用域
cout << "Base下的m_A = " << s.Base::m_A << endl;
//同名成员函数的处理方式
s.func(); //直接调用,调用的是子类中的同名成员
//如何调用父类的,加上作用域
s.Base::func();
//对父类进行重载
//如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类所有的同名成员函数, 如果想访问到父类中被隐藏的同名成员函数,需加作用域
s.Base::func(10);
}
1.9.3、继承同名静态成员处理方式
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致。
- 访问子类同名成员,直接访问即可
- 访问父类同名成员 需要加作用域
class Base
{
public:
static void func()
{
cout<<“Base-static void func()”<<endl;
}
static void func(int a)
{
cout<<"Base-static void fubc(int a)"
}
static int m_A;
};
int Base::m_A = 100; //静态成员变量需要在类外进行初始化
class son:public Base
{
public:
static void func()
{
cout<<"son—static void func()"<<endl; //静态成员变量需要在类外进行初始化
}
static int m_A;
};
int son::m_A = 200;
//同名成员属性
void test01()
{
//通过对象访问
cout<<“通过对象访问:”<<endl;
son s;
cout<< “son下的m_A=” << s.m_A <<endl;
cout<< “Base下的m_A=” << s.Base::m_A <<endl;
//通过类名进行访问
cout << "通过类名访问: " << endl;
cout << "Son 下 m_A = " << Son::m_A << endl;
cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}
//同名成员函数
void test02()
{
//通过对象访问
cout << "通过对象访问: " << endl;
son s;
s.func(); //子类成员函数,子类直接访问,符类加作用域
s.Base::func(); //父类成员函数,子类直接访问,父类加作用域
//通过类名进行访问,父类也需要加作用域
cout << "通过类名访问: " << endl;
Son::func();
Son::Base::func();
//父类重载的func(int)成员函数的调用
//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问
Son::Base::func(100);
}
int main()
{
//test01();
test02();
system("pause");
return 0;
}
注意:在继承静态成员时,同样遵循继承基类和派生类的上下级关系。
#include <iostream>
using namespace std;
class Base{
public:
static void func()
{
cout << "Base - static void func()" << endl;
}
static void func(int a)
{
cout << "Base-static void fubc(int a)";
}
protected:
static int m_B;
};
int Base::m_B = 100;
class son : public Base
{
public:
void test02() {
Base::m_B = 300;
cout << Base::m_B << endl;
}
static void func()
{
cout << "son—static void func()" << endl; //静态成员变量需要在类外进行初始化
}
static int m_A;
};
int son::m_A = 200;
void test01() {
//son::Base::m_B = 200;
}
int main()
{
son s1;
s1.test02();
system("pause");
return 0;
}
1.9.4、多继承语法
c++允许一个类继承多个类
语法: class子类:继承方式 父类1,继承方式 父类2…
多继承可能会引起父类中的同名成员的出现,需要加作用域进行区分。
C++实际开发中不建议用多继承
class Base1
{
public:
Base1()
{
m_A=100;
}
public:
int m_A;
};
class Base2
{
public:
Base2()
{
m_A=200;
}
public:
int m_A;
};
//语法:class 子类:继承方式 父类1 ,继承方式 父类2 …
class son :public Base2,public Base1
{
public:
son()
{
m_C=300;
m_D=400;
}
public:
int m_C;
int m_D;
};
//多继承容易产生成员同名的情况
//使用类名作用域 可以区分调用哪一个基类的成员
void test01()
{
son s;
cout<<“sizeof son=”<<sizeof(s)<<endl;
cout<<s.Base1::m_A<<endl;
cout<<s.Base2::m_A<<endl;
//加上类名作用域
}
总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域。
1.9.5、菱形继承
菱形继承概念:
- 两个派生类继承同一个基类
- 又有某个类同时继承两个派生类
典型的菱形继承案例:
菱形继承问题:
- 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,会产生二义性;
- 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
示例:
class Animal
{
public:
int m_Age;
};
class sheep : public Animal{};
class Tuo : public Animal{};
class sheepTuo : public sheep,public Tuo{};
void test01()
{
sheepTuo st;
st.sheep::m_Age = 18;
st.Tuo::m_Age = 28;
//当菱形继承,两个父类拥有相同数据,需要加以作用域区分
cout<<“st.sheep::m_Age=“<<st.sheep::m_Age<<endl;
cout<<“st.Tuo::m_Age=“<<st.Tuo::m_Age<<endl;
//这份数据我们知道只有一份就可以,菱形继承导致数据有两份,资源浪费。
}
看一下sheepTuo st的对象模型:
拷贝路径:
找到vs开发人员命令提示符:
打开之后进行跳转盘符:
cd到刚才赋值的路径下:
cd到这个路径之后,输入dir,看一下文件目录
继续输入: c1 /d1 report SingleClassLayoutsheepTuo"08 菱形继承.cpp"
我们可以看到,现在数据确实有两份:
那么怎么解决这个继承两份数据的问题呢?
//利用虚继承解决菱形继承的问题 在继承之前加上关键字virtual
最大的一个类叫做虚基类,虚继承解决菱形继承问题
此时结果变为28,就是m_Age数据只有一份了,类似于共享了;
再加上这个:
也可以正常输出;只有一份数据;
此时我们再看内存中的son的对象模型:
vbptr: 虚基类指针,会指向虚基类表vbtable
虚基类表中记录的是偏移量,虚基类指针加上偏移量就可以找到
现在就是继承的数据只有一份。
1.10、多态
多态是c++面向对象三大特性之一
多态分为两类(多态就是有多种形态)
- 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
class animal
{
public:
//speak函数就是虚函数
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数的调用了,
//virtual加上就是一个指针
virtual void speak()
{
cout<<“动物在说话”<<endl;
}
};
class cat: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;
}
总结:
多态满足条件:
- 有继承关系
- 子类重写父类中的虚函数
多态使用条件
- 父类指针或引用指向子类对象
1.10.1、纯虚函数与抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为 纯虚函数;
纯虚函数语法:virtual 返回值类型 函数名(参数列表)=0;
类中有了纯虚函数,这个类也被称为:抽象类。
抽象类特点:
-
无法实例化对象
-
子类必须重写抽象类中的纯虚函数,否则也属于抽象类
class Base
{
public:
//纯虚函数
//类中只要有一个纯虚函数就称为抽象类
//抽象类无法实例化对象
//子类必须重写父类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
virtual void func() //子类进行重写父类的纯虚函数
{
cout << "func调用" << endl;
};
};
void test01()
{
Base * base = NULL;
//base = new Base; // 错误,抽象类无法实例化对象
base = new Son;
base->func();
delete base;//记得销毁
}
int main()
{
test01();
system("pause");
return 0;
}
1.10.2、虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
虚析构和纯虚析构共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象。
虚析构语法:
virtual ~类名(){};
纯虚析构语法:
virtual ~类名()=0;
类名:: ~类名(){};
class Animal
{
public:
Animal()
{
cout << "Animal 构造函数调用!" << endl;
}
virtual void Speak() = 0;
//虚析构函数:析构函数加上virtual关键字,
virtual ~Animal() {
cout << "Animal虚析构函数调用!" << endl;
}
//纯虚析构函数,在下面外面实现
virtual ~Animal() = 0; //必须要有实现
//父类的纯虚析构函数,当我们将其改为纯虚析构时,就会调用子类的析构函数。
Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用!" << endl;
}
//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。
class Cat : public Animal
{
public:
Cat(string name) //构造函数
{
cout << "Cat构造函数调用!" << endl;
m_Name = new string(name); //在堆区开辟一个地址,将名字放入,用指针进行接收。
}
virtual void Speak() //子类重写父类的纯虚函数
{
cout << *m_Name << "小猫在说话!" << endl;
}
~Cat() //子类的析构函数
{
cout << "Cat析构函数调用!" << endl;
if (this->m_Name != NULL)
{
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name; // 在子类中定义指针,放在堆区
};
如果没有虚析构函数:
父类指针在析构时候 不会调用子类的析构函数 导致子类如果有堆区的属性,出现内存泄漏。
子类的析构函数应该在Animal的析构函数之前进行调用;
解决方法: 将父类的析构函数变成虚析构
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
- 拥有 纯虚析构的类也属于抽象类。