类和对象(封装和对象的初始化和清理)
C++具有面向对象的三大特性:封装,继承,多态。C++认为万事万物都是对象,都具有其行为和属性。任何事物都可以定义属性和行为,那么根据这个特点,我们可以按照以下方法定义。
封装
用以下语句完成封装:class {访问权限: 行为/属性};
封装可以把生活中的事物,用C++抽象出来,写出其属性和行为。并且可以用访问权限加以权限控制{private
,protected
,public
}。
例如以下代码
class Circle
{
public:
int r;//半径
int ZC;//周长
private:
protected:
}
在C++中我们把通过一个类创建一个具体的对象的过程叫做实例化。
三种权限在使用过程中分别发挥这不同的作用,譬如说:public权限无论类的内外均有权访问,protected和private在类外无权访问。后两者区别在继承中会有不同。
类比C语言中的struct(结构体),其和class不同的是默认访问权限不同,struct默认为public,class默认为private。
对象的初始化和清理-(构造函数和析构函数)
对于对象而言,初始化和格式化是个非常重要的安全问题,在当今这个对于保护个人隐私非常重要的时代,我们有必要学习一下(无端联想)。
C++语言利用构造函数的析构函数来解决上述问题,这两种函数在没有我们定义的时候会由编译器自动调用,完成初始化和清理工作。
对象的初始化和清理工作是必须要做的事情,如果我们没有提供构造和析构函数,编译器会提供这两个函数为空实现。
- 构造函数:在创建对象的时候自动调用的函数。
- 析构函数:在对象被销毁前自动调用,执行一些清理工作。
构造函数语法: 类名() {}
- 可以有参数,不写void,没有返回值,函数名和类名相同。
析构函数语法:~类名() {}
- 不可以有参数,不写void,没有返回值,函数名和类名相同。
以下是代码示例和运行结果
//1-1
#include<iostream>
using namespace std;
class Example_e
{
public:
Example_e()//构造函数
{
cout<<"构造函数调用"<<endl;
}
~Example_e()//析构函数
{
cout<<"析构函数调用"<<endl;
}
};
void example01()
{
Example_e ex1;
}
int main()
{
example01();
return 0;
}
运行结果:
构造函数调用
析构函数调用
由于ex1是局部变量存放于栈区,在调用结束后就会被自动释放,所以,构造函数和析构函数都被调用了。
构造函数的分类及调用
构造函数分为有参构造,无参构造,普通构造,拷贝构造(复制构造也对)。
在定义构造函数的时候有参数就叫做有参构造。
有以下代码:
//1-2
#include<iostream>
using namespace std;
class example
{
public:
example(int a)
{
cout<<"现在是有参调用"<<endl;
}
example()
{
cout<<"现在是无参调用"<<endl;
}
//拷贝构造函数
example (const example &xx)
{
cout << "拷贝构造函数!" << endl;
}
//析构函数
~example()
{
cout << "析构函数!" << endl;
}
};
void example01()
{
cout<<"现在调用无参构造函数"<<endl;
example exp1;
cout<<"调用完毕1"<<endl;
}
void example02()
{
cout<<"现在调用有参构造函数"<<endl;
//第一种调用方式
example exp2(10);
//注意1:调用无参构造函数不能加括号,如果加了编译器认为这是一个函数声明
//example exp2();
cout<<"完毕2"<<endl;
//第二种调用方法
example exp3 = example(10);
cout<<"完毕3"<<endl;
example exp4 = example(exp2);
//2.3 隐式转换法
example p4 = 10; // example p4 = example(10);
example p5 = p4; // example p5 = example (p4);
cout<<"完毕4"<<endl;
//注意2:不能利用 拷贝构造函数 初始化匿名对象 编译器认为是对象声明
//example p5(p4);
}
int main()
{
example01();
example02();
return 0;
}
运行结果一目了然:
现在调用无参构造函数
现在是无参调用
调用完毕1
析构函数!
现在调用有参构造函数
现在是有参调用
完毕2
现在是有参调用
完毕3
拷贝构造函数!
现在是有参调用
拷贝构造函数!
完毕4
析构函数!
析构函数!
析构函数!
析构函数!
析构函数!
一行一行读下去,自然就懂了,不懂的话,欢迎Github提交一个issue。
什么时候使用拷贝(复制)构造函数
- 使用一个已经创建完毕的对象来初始化一个新的对象。
- 值传递的形式给函数参数传值。
- 以值的方式传回局部对象。
简简单单地来一个程序示例吧
//1-3-1
#include<iostream>
using namespace std;
class example
{
public:
//无参(默认)构造函数
example() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
example(int a) {
number = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
//example(const example& p) {
//number = p.number;
//cout << "拷贝构造函数!" << endl;
//}
//析构函数
~example() {
cout << "析构函数!" << endl;
}
int number;
};
void test01()
{
example p1(18);
//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
example p2(p1);
cout << "这个数字是 " << p2.number << endl;
}
int main()
{
test01();
return 0;
}
运行结果:
有参构造函数!
这个数字是 18
析构函数!
析构函数!
这个里面我们能看到如果我把拷贝构造函数注释掉,他会以值传递的形式把p1传给p2.
假设我们只提供拷贝构造函数会发生什么呢?
//1-3-2
#include<iostream>
using namespace std;
class example
{
public:
//无参(默认)构造函数
//example() {
// cout << "无参构造函数!" << endl;
//}
//有参构造函数
//example(int a) {
// number = a;
// cout << "有参构造函数!" << endl;
//}
//拷贝构造函数
example(const example& p) {
number = p.number;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~example() {
cout << "析构函数!" << endl;
}
int number;
};
/*
void test01()
{
example p1(18);
//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
example p2(p1);
cout << "这个数字是 " << p2.number << endl;
}
*/
void test02()
{
//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
example p1; //此时如果用户自己没有提供默认构造,会出错
example p2(10); //用户提供的有参
example p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供
//如果用户提供拷贝构造,编译器不会提供其他构造函数
example p4; //此时如果用户自己没有提供默认构造,会出错
example p5(10); //此时如果用户自己没有提供有参,会出错
example p6(p5); //用户自己提供拷贝构造
}
int main()
{
test02();
return 0;
}
他是一定会报错的:
类“example”不存在默认构造函数
没有与参数列表匹配的构造函数"example::exmaple"实例
如果我提供有参构造和拷贝构造呢(代码都差不多,就是加了注释)
//1-3-3
#include<iostream>
using namespace std;
class example
{
public:
//无参(默认)构造函数
//example() {
// cout << "无参构造函数!" << endl;
//}
//有参构造函数
example(int a) {
number = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
example(const example& p) {
number = p.number;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~example() {
cout << "析构函数!" << endl;
}
int number;
};
/*
void test01()
{
example p1(18);
//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
example p2(p1);
cout << "这个数字是 " << p2.number << endl;
}
*/
void test02()
{
//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
example p1; //此时如果用户自己没有提供默认构造,会出错
example p2(10); //用户提供的有参
example p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供
//如果用户提供拷贝构造,编译器不会提供其他构造函数
example p4; //此时如果用户自己没有提供默认构造,会出错
example p5(10); //此时如果用户自己没有提供有参,会出错
example p6(p5); //用户自己提供拷贝构造
}
int main()
{
test02();
return 0;
}
类“example”不存在默认构造函数
所以有了有参构造函数,就不会提供默认构造函数(无参)
都提供了,运行结果如下:
//1-3
#include<iostream>
using namespace std;
class example
{
public:
//无参(默认)构造函数
example() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
example(int a) {
number = a;
cout << "有参构造函数!" << endl;
}
//拷贝构造函数
example(const example& p) {
number = p.number;
cout << "拷贝构造函数!" << endl;
}
//析构函数
~example() {
cout << "析构函数!" << endl;
}
int number;
};
/*
void test01()
{
example p1(18);
//如果不写拷贝构造,编译器会自动添加拷贝构造,并且做浅拷贝操作
example p2(p1);
cout << "这个数字是 " << p2.number << endl;
}
*/
void test02()
{
//如果用户提供有参构造,编译器不会提供默认构造,会提供拷贝构造
example p1; //此时如果用户自己没有提供默认构造,会出错
example p2(10); //用户提供的有参
example p3(p2); //此时如果用户没有提供拷贝构造,编译器会提供
//如果用户提供拷贝构造,编译器不会提供其他构造函数
example p4; //此时如果用户自己没有提供默认构造,会出错
example p5(10); //此时如果用户自己没有提供有参,会出错
example p6(p5); //用户自己提供拷贝构造
}
int main()
{
test02();
return 0;
}
无参构造函数!
有参构造函数!
拷贝构造函数!
无参构造函数!
有参构造函数!
拷贝构造函数!
析构函数!
析构函数!
析构函数!
析构函数!
析构函数!
析构函数!
说明了当我们已经定一个拷贝构造函数的时候确实系统不会自动生成一个默认构造函数
构造函数的时候,在默认的情况之下,编译器会自动生成三个函数
默认构造函数,默认析构函数,默认拷贝构造函数。前两者在默认的情况下会默认无参数,函数体为空,不执行任何操作,对于默认拷贝构造函数则会以值传递的形式复制一份。
如果用户已经定义了一个有参构造函数,则不会提供一个默认无参构造函数,但是仍然会提供一个拷贝构造函数。但是如果用户已经定义了一个拷贝构造函数,则程序不会提供其他的构造函数。
深拷贝和浅拷贝
在之前的注释中,我们应该可以看到一个名词叫作浅拷贝,顾名思义,浅的拷贝,就是简单的赋值拷贝操作。深拷贝就是我们要向堆区申请空间,并进行拷贝操作。
请看这个代码:
//1-4
#include<iostream>
using namespace std;
class example
{
public:
public:
//无参(默认)构造函数
example() {
cout << "无参构造函数!" << endl;
}
//有参构造函数
example(int number01 ,int number02) {
cout << "有参构造函数!" << endl;
number1 = number01;
number2 = new int(number02);
}
//拷贝构造函数
example(const example& p) {
cout << "拷贝构造函数!" << endl;
//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
number1 = p.number1;
number2 = new int(*p.number2);
}
//析构函数
~example() {
cout << "析构函数!" << endl;
if (number2 != NULL)
{
delete number2;
}
}
int number1;
int *number2;
};
void test01()
{
example p1(18, 180);
example p2(p1);
cout << "p1数一:" << p1.number1 << "数二" << *p1.number2 << endl;
cout << "p1数一:" << p2.number1 << " 数二: " << *p2.number2 << endl;
}
int main()
{
test01();
return 0;
}
运行结果:
有参构造函数!
拷贝构造函数!
p1数一:18数二180
p1数一:18 数二: 180
析构函数!
析构函数!
这个代码是正确的,假设我们在拷贝构造函数的时候没有开辟新的内存分区,没有使用new,那么会发生什么呢?
实测VSCODE确实可以运行!也确实,这玩意有点问题。
然而同样的代码在Visual studio上不可运行,没有进行深拷贝确实会带来一个非常严重的重复释放的问题。
已执行断点指令。
初始化列表
C++可以用初始化列表来初始化属性。
它的语法是这样的:构造函数():属性1(值1),属性2(值2)... {}
,以下是一段具体例子
class example {
public:
example(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {
}
private:
int m_A;
int m_B;
int m_C;
}
可以用来初始化属性,具体可以自行尝试如何使用。
类对象作为成员
C++类中的成员可以是另一个类的对象,我们称该成员为 对象成员
例如:
class A {}
class B
{
A a;
}
B类中有对象A作为成员,A为对象成员
///1-5
#include<iostream>
#include<string>
using namespace std;
class example1
{
public:
string exp1;
example1()
{
cout<<"example1无参构造"<<endl;
}
//1有参构造函数
example1(string name1)
{
exp1=name1;
cout<<"example1 构造函数调用"<<endl;
}
//1析构函数
~example1()
{
cout<<"example1 析构函数调用"<<endl;
}
};
class example2
{
public:
string exp2;
example2()
{
cout<<"example2无参构造"<<endl;
}
//2有参构造函数
example2(string name2 , string name3)
{
exp2=name2;
ex1=name3;
cout<<"example2构造函数调用"<<endl;
}
//2析构函数
~example2()
{
cout<<"example2析构函数调用"<<endl;
}
void print()
{
cout<<"1内容"<<ex1.exp1<<"2内容"<<exp2<<endl;
}
example1 ex1;
};
void test01()
{
//当类中成员是其他类对象时,我们称该成员为 对象成员
//构造的顺序是 :先调用对象成员的构造,再调用本类构造
//析构顺序与构造相反
example2 exp3("ljt","ljx");
exp3.print();
}
int main()
{
test01();
return 0;
}
结果:
example1无参构造
example1 构造函数调用
example1 析构函数调用
example2构造函数调用
1内容ljx2内容ljt
example2析构函数调用
example1 析构函数调用
可见先调用example1,再调用example2,析构顺序相反。
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
静态成员变量:
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数:
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
有以下两个代码:
静态成员变量
//1-6-1 静态成员变量
#include<iostream>
using namespace std;
class example{
public:
static int a;//静态成员变量是共享的,类内声明,类外初始化,所有对象都可以访问这个数据
private:
static int b;//静态成员变量也是可以有访问权限的
};
int example::a = 10;
int example::b = 10;
int main()
{
cout<<example::a<<endl;
//第一种访问方式
example::a = 60;
cout<<example::a<<endl;
//第二种
example exp1;
example exp2;
exp1.a=20;
cout<<"类"<<example::a<<endl;
cout<<"exp1:"<<exp1.a<<endl;
cout<<"exp2:"<<exp2.a<<endl;
exp2.a =30;
cout<<"类"<<example::a<<endl;
cout<<"exp1:"<<exp1.a<<endl;
cout<<"exp2:"<<exp2.a<<endl;
//cout<<"b的值"<<endl;
//cout<<example::b<<endl; 报错:成员 \"example::b\" (已声明 所在行数:11) 不可访问
return 0;
}
运行结果
10
60
类20
exp1:20
exp2:20
类30
exp1:30
exp2:30
静态成员变量是共享的
静态成员函数
#include<iostream>
using namespace std;
class example
{
public:
//静态成员函数特点:
//1 程序共享一个函数
//2 静态成员函数只能访问静态成员变量
static void func()
{
cout << "func调用" << endl;
a= 100;
//b = 100; //错误,非静态成员引用必须与特定对象相对
}
static int a; //静态成员变量
int b; //
private:
//静态成员函数也是有访问权限的
static void func2()
{
cout << "func2调用" << endl;
}
};
int example::a = 10;
int main()
{
//访问方式两种
example::func();
example exp1;
exp1.func();
//example::func2(); 函数 "example::func2" (已声明 所在行数:24) 不可访问
}
输出结果:
func调用
func调用