C++面向对象的三大特性:封装、继承、多态
C++认为万事万物皆为对象,对象上有其属性和行为
例如:
人可以作为对象,属性有姓名、年龄、身高、体重。。。行为有走、跑、跳、吃饭、唱歌。。。
具有相同性质的对象,我们可以抽象为类,人属于人类,车属于车类。
封装:封装是C++面向对象三大特性之一,其将属性和行为作为一个整体,表现生活中的事物;并可以将属性和行为加以权限控制
封装意义一:在设计类的时候,属性和行为写在一起,表现事物
语法:class 类名{访问权限:属性/行为};
示例1:设计一个圆类
#include
using namespace std; //圆周率 const double PI = 3.14; //设计一个圆类,求圆的周长 //圆求周长的公式:2*PI*半径 //class 代表设计一个类,类后面紧跟着的就是类的名称 class Circle { public: //属性:半径 int m_r; //行为:获取圆的周长 double calculateZC() { return 2 * PI * m_r; } }; int main() { //通过这个圆类创建具体的圆(对象) Circle c1; //给圆对象的属性进行赋值 c1.m_r = 10; cout << "圆的周长为:" << c1.calculateZC() << endl; system("pause"); return 0; } 结果:圆的周长为:62.8
示例二:设计一个学生类,属性有姓名和学号,可以给姓名和学号赋值,可以显示学生的姓名和学号
#include
#include using namespace std; //设计学生类 class Student { public://公共权限 //属性 string m_Name;//姓名 int m_Id;//学号 //行为 void showStudent() { cout << "姓名:" << m_Name << "学号:" << m_Id << endl; } }; int main() { //实例化 Student s1; s1.m_Name = "张三"; s1.m_Id = 1; s1.showStudent(); system("pause"); return 0; } 结果:姓名:张三学号:1
对于上面的代码,可以改进一下:
#include
#include using namespace std; //设计学生类 class Student { public://公共权限 //属性 string m_Name;//姓名 int m_Id;//学号 //行为 void showStudent() { cout << "姓名:" << m_Name << "学号:" << m_Id << endl; } void setName(string name) { m_Name = name; } void setId(int id) { m_Id = id; } }; int main() { //实例化 Student s1; s1.setName("张三"); s1.setId(1); s1.showStudent(); system("pause"); return 0; }
封装意义二:类在设计时,可以把属性和行为放在不同的权限下,加以控制
访问权限有三种:
英文名 | 中文名 | 说明 |
---|---|---|
public | 公共 | 成员类内可以访问、类外也可访问 |
protected | 保护 | 成员类内可以访问、类外不可访问,但是在继承时,protected的内容可以被子类访问 |
private | 私有权限 | 成员类内可以访问、类外不可访问,但是在继承时,private的内容不可以被子类访问 |
在C++中和struct和class唯一的区别就在于默认的访问权限不同;对于struct
来说,其默认权限为公共
;而对于class
来说,其默认权限为私有
。
这块知识就和java中的封装对应上了,在java中,我们时常用private对类中的成员属性进行私有化,然后用set去写入,用get去读。
在C++中,我们同样的将所有成员属性设置为私有,这样的好处是可以自己控制读写权限,并且在写入数据的过程中,我们还可以用函数来控制其写入数据的有效性。
如果想体验这个知识点,可以试着做一下下面的案例:
#include
#include using namespace std; //设计人类 class Person { private: //姓名 string m_Name; //年龄 int m_Age; public: void setName(string name) { m_Name = name; } void setAge(int age) { m_Age = 0; if(age>0 && age < 200) m_Age = age; } string getName() { return m_Name; } int getAge() { return m_Age; } }; int main() { Person p1; p1.setName("张三"); p1.setAge(-1); cout << p1.getName() << endl; cout << p1.getAge() << endl; } 结果:张三
0
生活中我们买的电子产品都基本会有出厂设置,在某一天我们不用时候也会删除一些自己信息数据保证安全,而对于C++来说,C++的面向对象编程也是来源于生活,所以每个对象都会有初始设置以及对象销毁前的清理数据的设置。
C++利用了构造函数和析构函数解决上面说的初始化和销毁化问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。对象的初始化和清理工作是编译器强制要我们做的事。因此如果我们不提供构造和析构,编译器会提供编译器提供的构造函数和析构函数。并且两个都是是空实现。
和java实际上很类似,java中也存在构造函数,但是没有析构函数,java如果没有构造方法,那么其自带一个无参构造;如果写了构造方法,那么系统原有自带的无参构造会消失,这也意味着我们写完有参构造后还要再写一个无参构造。
java没有析构函数的原因是,其运行在JVM上,当对象消失,那么JVM会启用垃圾回收机制,回收分配给对象的资源;而C++不会自动回收,因此当对象消失时,需要提供析构函数来回收资源。
构造函数语法为类名(){}
,需要注意的是,C++中的构造函数没有返回值也不用写void,构造名称和类名相同。构造函数可以有参数,所以可以发生重载,即可以同时拥有空参构造方法
和有参构造方法
。程序在调用对象的时候会自动调用构造,无需手动调用,而且只会调用一次。
对于析构函数来说,其语法为~类名(){}
,需要注意的是,析构函数同构造函数的特点基本一样,有一点不一样的是析构函数不可以有参数,因此不能写多个析构函数。
在visual studio中,如果你用了系统提示的class创建,那么他会帮你把构造和析构全部写好,如果要写多余的构造自己补充即可。
两种分类方式:
按参数分 | 按类型分 |
---|---|
有参构造 | 普通构造 |
无参构造(默认构造) | 拷贝构造 |
三种调用方式:
拷贝构造
除了拷贝构造之外,其他的都是普通构造。在学习其他编程语言的过程中,我们并没有听过拷贝构造,其是C++中特有的一种构造方法,通过
类名 (const 类名 &对象名){拷贝内容}
即可定义拷贝构造,拷贝构造常用语将某一个对象的属性拷贝给另外一个对象。
括号法
括号法很简单,但是学过java的同学可能会混,什么意思呢。
如果是调用无参构造,那么只需
类名 对象名
,系统会自动调用,而在java中,我们通常是类名 对象名 = new 构造器()
,也就是说即使是无参我们也会把括号写上;而在C++中调用无参构造时不能写括号,因为写了括号,C++会误以为你是写了一个函数的声明。如果是有参构造,只需要
类名 对象名(参数)
即可。如果是拷贝构造,只需要
类名 对象名(被拷贝的对象)
。
显式法
如果不用括号法,可以用显式法来调用构造函数,从形式上看,显式法更像java的构造调用。
对于无参构造,显式法和括号法一样。
对于有参构造,显式法格式为:
类名 对象名 = 类名(参数)
对于拷贝构造,显式法格式为:
类名 对象名 = 类名(被拷贝的对象)
。需要注意的是,
类名(参数)
类似于java学习中的匿名内部类
,在C++中被我们称为匿名对象,其特点是当前行执行结束后,系统会立即回收掉匿名对象。还有一点是:不要利用拷贝函数来初始化一个匿名对象,如:
类名(对象名)
,如果你尝试这么做,编译器会认为其等价于类名 对象名
,即对象的声明。
隐式转换法
隐式转换法实际上是显式法的简略版,但是你说简略实际上它还没括号法简略呢,所以不想了解也没啥问题,虽然别人写的代码你可能看不懂哈哈。
对于有参构造:隐式转换法格式为:
类名 对象名 = 参数
。对于拷贝构造也是同样的原理。
C++中需要拷贝构造函数的情况通常有三种:
示例:
#include
using namespace std;
class Person
{
public:
int m_Age;
Person()
{
cout << "Person默认构造函数调用" << endl;
}
Person(int age)
{
cout << "Person有参构造函数调用" << endl;
m_Age = age;
}
Person(const Person& p)
{
cout << "Person拷贝构造函数调用" << endl;
m_Age = p.m_Age;
}
~Person()
{
cout << "Person默认析构函数调用" << endl;
}
};
//构造拷贝函数调用时机
//1、使用一个已经创建完毕的对象来初始化一个新对象
void test01()
{
Person p1(20);
Person p2(p1);
cout << "p2的年龄为:" << p2.m_Age << endl;
}
//2、值传递的方式给函数参数传值
void doWork(Person p)
{
}
void test02()
{
Person p;
doWork(p);
}
//3、值方式返回局部对象
Person doWork2()
{
Person p1;
return p1;
}
void test03()
{
Person p = doWork2();
}
int main()
{
//test01();
//test02();
test03();
}
默认情况下,C++编译器至少给一个类添加三个函数:
构造函数调用规则如下:
乍一看,上面的规则似乎和Java中的很类似。
如果想要了解上述的知识点,我们可以手动做一下下列的案例:
#include
using namespace std;
//构造函数的调用规则
/*- 默认构造函数,无参,函数体为空
- 默认析构函数,无参,函数体为空
- 默认拷贝构造函数,对所有属性值进行拷贝*/
class Person
{
public:
int m_Age;
Person()
{
cout << "Person的默认构造函数调用" << endl;
}
Person(int age)
{
cout << "Person的有参构造函数调用" << endl;
}
/*person(const person &p)
{
cout << "person的拷贝构造函数调用" << endl;
m_age = p.m_age;
}*/
~Person()
{
cout << "Person的默认析构函数调用" << endl;
}
};
void test01()
{
Person p;
p.m_Age = 18;
Person p2(p);
cout << "p2的年龄为:" << p2.m_Age << endl;
}
int main()
{
test01();
}
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新生成空间,然后再进行拷贝
深浅拷贝是面试最常见的问题,需要严加重视。
我们现在来思考一个问题,如果我们想要利用拷贝构造函数在堆区生成一块空间怎么办?一般思路应该如下:
#include
using namespace std;
class Person
{
public:
int m_Age;
int* m_Height;
Person()
{
cout << "Person的默认构造函数被调用" << endl;
}
Person(int age, int height)
{
cout << "Person的有参构造函数调用" << endl;
m_Age = age;
m_Height = new int(height);
}
~Person()
{
if (m_Height != NULL)
{
delete(m_Height);
m_Height = NULL;
}
cout << "Person的析构函数调用";
}
private:
};
void Test01()
{
Person p1(18,160);
cout << "p1的年龄为:" << p1.m_Age << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.m_Age << endl;
}
int main()
{
Test01();
}
经过上面的代码敲试,你会发现这段代码是会报错的。因为这里出现了一个问题——重复释放内存。
我们知道,上述代码的p2是由p1拷贝而来,在执行析构函数时,p2的代码写于p1之后,而析构函数位于栈空间,那么根据栈后入先出
的特点,则p2应该首先执行析构函数。
在你没有编写拷贝构造函数时,系统会默认给出一个拷贝构造函数,这个拷贝构造函数执行的是一个浅拷贝的过程,相当于我们平时说的值传递,它的作用是赋值p1对象里所有的值给p2。
当我们执行上面的代码时,p1的析构也给了p2,p1中的m_Height作为在堆区开辟的数据,p2从p1拷贝了m_Height的指针,也就是说,P2.m_Height和P1.m_Height共享这个指针。当p2先执行析构,该指针就被释放掉;而轮到p1执行析构时,没有指针可释放,所以就报错了。编译器写的浅拷贝构造函数如下:
Person(const Person &p)
{
cout<<"Person 拷贝构造函数调用"<
既然弄明白了上述的问题,我们要做的,实际上就是要再开辟一块堆区空间,使得p2和p1的m_Height空间不一样。如下所示:
#include
using namespace std;
class Person
{
public:
int m_Age;
int* m_Height;
Person()
{
cout << "Person的默认构造函数被调用" << endl;
}
Person(int age, int height)
{
cout << "Person的有参构造函数调用" << endl;
m_Age = age;
m_Height = new int(height);
}
~Person()
{
if (m_Height != NULL)
{
delete(m_Height);
m_Height = NULL;
}
cout << "Person的析构函数调用";
}
Person(const Person& p)
{
cout << "Person 拷贝构造函数调用" << endl;
m_Age = p.m_Age;
//深拷贝
m_Height = new int(*p.m_Height);
}
private:
};
void Test01()
{
Person p1(18,160);
cout << "p1的年龄为:" << p1.m_Age << endl;
cout << "p1的身高为:" << p1.m_Height << endl;
Person p2(p1);
cout << "p2的年龄为:" << p2.m_Age << endl;
cout << "p2的身高为:" << p2.m_Height << endl;
}
int main()
{
Test01();
}
我们知道构造函数实际上就是用于初始化对象,C++提供了初始化列表语法
,用于初始化对象中的属性。其语法如下所示:
构造函数():属性1(值1),属性2(值2)…
#include
using namespace std;
//初始化列表
class Person
{
public:
int m_A;
int m_B;
int m_C;
//传统初始化方法
/*Person(int a, int b, int c)
{
m_A = a;
m_B = b;
m_C = c;
};*/
//初始化列表
/*Person() :m_A(10), m_B(20), m_C(30)
{
}*/
//初始化列表
Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c)
{
}
};
void test01()
{
//Person p(10, 20, 30);
Person p(10,20,30);
cout << "m_A=" << p.m_A << endl;
cout << "m_B=" << p.m_B << endl;
cout << "m_C=" << p.m_C << endl;
}
int main()
{
test01();
}
C++类中的成员可以是另一个类的对象,我们称该成员为对象成员。
如:
class A{}
class B
{
A a
}
由此引发一个问题,当创建B对象时,A和B的构造和析构的顺序谁先谁后。
#include
using namespace std;
#include
class Phone
{
public:
//手机品牌名称
string m_PName;
Phone(string pName)
{
m_PName = pName;
};
private:
};
class Person
{
public:
//姓名
string m_Name;
//手机
Phone m_Phone;
Person(string name, string pName) :m_Name(name), m_Phone(pName)
{
}
private:
};
void test01()
{
Person p("张三", "苹果X");
cout << p.m_Name << "拿着" << p.m_Phone.m_PName << endl;
}
int main()
{
test01();
}
执行以上的代码我们可以看出,实际上对象成员是先构造的,而后类对象再构造;而对于析构来说,类对象先析构,而后是对象成员。这就好比搭积木,你要搭个人出来肯定要先搭好胳膊和腿;同理,如果你要拆积木,也肯定是先拆胳膊和腿。