在C++语言中,我们使用类定义自己的的数据类型。通过定义新的类型来反映待解决问题中的各种概念, 使得我们可以更容易编写,调试和修改程序。今天我们就初步的认识类,并学习一些类的相关知识。
面向对象程序设计的核心思想是数据的抽象,继承和动态绑定。通过使用数据抽象我们可以将类的接口与实现分离;使用继承,我们可以定义相似的类型并对相似关系进行建模;使用动态绑定,我们可以在一定程度上忽略相似类型的区别,而以统一的方式使用他们的对象。
我们前面不会涉及类的继承和动态绑定,这些会在我们有一定基础之后慢慢进行解锁。我们会对数据的抽象,也就是我们的封装进行相应的学习。
我们所使用的 C语言就是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
在C++中我们只需要把衣服和相应的东西放入洗衣机就可以,洗衣机会把洗好的衣服给我们,我们是不需要关心洗衣机是如何洗衣服和甩干的。
在C语言中的结构体只能定义变量,而在C++中,结构体内不仅可以定义变量,也可以定义函数。比如我们之前实现用C语言方式实现的链表,结构体中只能定义变量;但在C++中,我们也可以把相应的函数放入struct结构体中。
struct STU
{
void Init(char* name, int math, int chinese)//初始化函数
{
memcpy(_name, name, sizeof(char) * 10);
_math = math;
_chinese = chinese;
}
void Total_score()//计算总分
{
_total_score = _math + _chinese;
}
void print()//打印相关信息
{
cout << "姓名:" << _name << " 数学:" << _math << " 语文:" << _chinese << " 总分:" << _total_score << endl;
}
char _name[10];//学生姓名
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _total_score;//学生总成绩
};
int main()
{
struct STU stu;
char name[10] = "zhangsan";
stu.Init(name, 80, 80);//调用我们结构体中的初始化函数
stu.Total_score();//调用我们结构体中的计算总分函数
stu.print();//调用我们结构体中的打印函数
return 0;
}
上面结构体的定义中, 在C++中更喜欢用class(类关键字)来代替struct。struct和class的区别我们会在下面见到。
类的基本思想是 数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程(以及设计)技术。类的接口包括用户所能执行的操作:类的实现则包括类的数据成员,负责接口实现的函数体以及定义类所需的各种私有函数。封装实现了类的接口和实现分离,封装后的类隐藏了它的实现细节,也就是说类的用户只能使用接口而无法访问实现部分。
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。在C++中,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。从而实现我们数据的隐藏。
class className //className是类的名字,和我们上面结构体中的STU一样
{
// 类体:由成员函数和成员变量组成
};//和结构体一样,后面有分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,类定义结束时后面分号和结构体一样不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
class STU
{
public:
void Show()//成员函数
{
cout << "姓名:" << _name << " 数学:" << _math << " 语文:" << _chinese << endl;
}
private:
//成员变量
char _name[10];//学生姓名
int _math;//学生数学成绩
int _chinese;//学生语文成绩
void Init(char* name, int math, int chinese);//成员函数
};
类的两种定义方式:
细心的读者可能已将发现了在类中的pbulic和private,这是什么东西呢?作用又是什么嘞?
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。而上面的就是我们访问权限。
public(公有):公有修饰的成员在类外可以直接被访问
protected(保护):类外不可以访问,自己的成员函数内可以访问。
private(私有):和保护相似,具体区别会在后面展开。
注意:访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
现在让我们解决上面的疑问,在C++中struct和class的区别: class的默认访问权限为private,而struct的默认访问权限为public(因为在C++中struct要兼容C)
类定义了一个新的作用域。
class STU
{
public:
void Show();//成员函数
private:
//成员变量
char _name[10];//学生姓名
int _math;//学生数学成绩
int _chinese;//学生语文成绩
};
//这里需要指定Show函数是属于STU这个类域
void STU::Show()//成员函数
{
cout << "姓名:" << _name << " 数学:" << _math << " 语文:" << _chinese << endl;
}
一个类就是一个作用域的事实能很好的解释为什么当我们在类的外部定义成员函数时必须提供类名和函数名。在类的外部,成员的名字被隐藏起来了。一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体,这样我们可以直接使用类的其他成员而无需再次授权了。
用类类型创建对象的过程,称为类的实例化。
类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它,而实例化出的对象 占用实际的物理空间,存储类成员变量,就比如:类实例化出对象就像现实中使用建筑设计图建造出房子,而类就像是设计图。
int main()
{
STU._math = 100;//编译失败:STU类是没有空间的,只有STU类实例化出的对象才有具体的成绩
struct STU stu;//类的实例化
return 0;
}
一个类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?
class STU
{
public:
void Show();//成员函数
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
};
int main()
{
cout << sizeof(STU) << endl;
return 0;
}
我们可以看到sizeof计算的结果只有我们成员变量的大小。
那下面的代码中的大小又是多少呢?
class STU
{
public:
void Show();//成员函数
private:
//成员变量
static int age;
int _math;//学生数学成绩
int _chinese;//学生语文成绩
};
int main()
{
cout << sizeof(STU) << endl;
return 0;
}
我们看到成员变量中 含有静态变量后的结果依然是8,这是为什么嘞?
在我们创建的类中:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。我们的成员函数和静态变量所有实例化出来的都可以使用,他们属于类的公共部分。
class STU
{
public:
void Init(int math, int chinese, int english)
{
_math = math;
_chinese = chinese;
_english = english;
}
void Total_score()//计算总分
{
_total_score = _math + _chinese + _english;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << " 总分:" << _total_score << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
int _total_score;//学生总成绩
};
int main()
{
STU stu1;
STU stu2;
stu1.Init(90, 85, 100);
stu2.Init(100, 100, 100);
stu1.Total_score();
stu2.Total_score();
stu1.Show();
stu2.Show();
return 0;
}
在上面的结论中,我们知道类的成员函数是公有的,那编译器是如何知道我们是stu1调用函数和stu2调用的函数呢?
C++中通过引入this指针解决该问题,C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
例如我们调用:
stu1.Total_score();
编译器负责把stu1的地址传给Total_score的隐式形参this,可以等价地认为编译器将该调用重写为下面形式:
//用于说明调用成员函数的实际执行过程,并不是真正的代码
STU::Total_score(&stu1);
通过调试我们可以看到this指针记录的是我们调用者的地址,this指针是编译器默认生成的,不需要我们显示传参。任何自定义命名为this的参数或者变量的行为都是非法的。
this指针的特性:
每个类定义了唯一的类型。对于两个类来说,即使他们的成员完全一样,这两个类也是两个不同类型。
class stu
{
void Init();
int _math;
};
class STU
{
void Init();
int _math;
};
int main()
{
stu stu1;
STU stu2 = stu1;//错误,stu1和stu2的类型不同
}
在上面的代码中,我们学生的成绩都是私有的,我们在类外是无法访问到的,那我们想要在类外访问它们又该如何做到呢?其方法是令其他类或者函数成为它的友元(friend)。
例如:
class STU
{
public:
friend void outInit(STU& stu, int math, int chinese);
void Init(int math, int chinese)
{
_math = math;
_chinese = chinese;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
};
void outInit(STU& stu, int math, int chinese)
{
stu._math = math;
stu._chinese = chinese;
}
int main()
{
STU stu1;
STU stu2;
stu1.Init(100, 100);//类成员函数调用
outInit(stu2, 90, 90);//普通函数调用
stu1.Show();
stu2.Show();
return 0;
}
我们使用普通函数时要对我们类进行传参,因为普通函数中没有我们的this指针,而类内的初始化就不需要我们对类进行传参。当我们把普通成员函数声明为类的友元时,我们的普通函数就可以访问我们的私有成员了。
一个类的有6个默认成员函数,当用户没有显式实现,编译器会生成的这些成员函数。
下面让我们来正式的认识这些默认的成员函数吧。
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象整个生命周期内只调用一次。
class STU
{
public:
STU(int math, int chinese = 0, int english = 0)//构造函数
{
_math = math;
_chinese = chinese;
_english = english;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
int main()
{
STU stu1(90, 85, 100);
STU stu2(80);
stu1.Show();
stu2.Show();
return 0;
}
由上面看我们的构造函数也可以是缺省函数。当我们对类进项实例化时自动调用了我们的构造函数。
构造函数是特殊的成员函数,构造函数的主要任务并不是开空间创建对象,而是初始化对象。
构造函数的特性如下:
下面代码为了方便,我们把自定义类型的成员变量设置为公有。
class STU
{
public:
STU()//构造函数
{}
STU(int math, int chinese = 0, int english = 0)//构造函数
{
_math = math;
_chinese = chinese;
_english = english;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
int main()
{
STU stu1;
STU stu2(90, 85, 100);
stu1.Show();
stu2.Show();
return 0;
}
观察上面的结果我们可以看出来,当我们stu1调用时调用的是没有参数的构造函数,所以stu1的数据都是随机值,而stu2调用的是重载函数,stu2中的值是我们所赋予的。
注意:
STU stu1;//对STU进行实例化得到stu1
STU stu2();//不可以实例化,编译器会认为一个函数的声明
我们第二种调用方式是错误的!!!
从构造函数的特性我们可以看到,当我们没有显示声明的构造函数,编译器会给我们生成一个。生成的结果是什么呢?
class PEO
{
public:
int _age;//年龄
int _height;//身高
};
class STU
{
public:
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
cout << "年龄:" << peo._age << " 身高:" << peo._height << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
PEO peo;//自定以类型
};
int main()
{
STU stu1;
stu1.Show();
return 0;
}
我们STU没有显示的提供构造函数,PEO也没有提供显示的构造函数,我们打印出来的结果都是随机值。
class PEO
{
public:
PEO()
{
_age = 20;
_height = 180;
}
int _age;//年龄
int _height;//身高
};
class STU
{
public:
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
cout << "年龄:" << peo._age << " 身高:" << peo._height << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
PEO peo;//自定以类型
};
int main()
{
STU stu1;
stu1.Show();
return 0;
}
我们STU没有显示的提供构造函数,PEO提供显示的构造函数,我们打印出来的结果STU里面的内置类型是随机值,而自定义类型调用了它自己的构造函数。
class PEO
{
public:
PEO()
{
cout << "PEO()" << endl;
_age = 20;
_height = 180;
}
int _age;//年龄
int _height;//身高
};
class STU
{
public:
STU()
{
cout << "STU()" << endl;
_math = 100;
_chinese = 100;
_english = 100;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
cout << "年龄:" << peo._age << " 身高:" << peo._height << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
PEO peo;//自定以类型
};
int main()
{
STU stu1;
stu1.Show();
return 0;
}
当我们都定义了构造函数时,我们进行实例化会调用对应构造函数,通过打印结果我们也可以看出。
从上面的案例和对应的结果我们可以发现:在C++中,默认构造函数对内置类型(如int,char等)类型不做处理,对自定义类型调用自定义类型的构造函数。这也是我们看到随机值的原因。
注意:C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class STU
{
public:
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
}
private:
//成员变量
int _math = 0;//学生数学成绩
int _chinese = 0;//学生语文成绩
int _english = 0;//学生英语成绩
};
int main()
{
STU stu1;
stu1.Show();
return 0;
}
注意:是在C++11 中可用!!!
对比下面函数打印的结果看一下吧:
为什么我们和上面不一样呢?我们没有给内置类型提供相应的构造函数,为什么我们内置类型的值也进行了初始化呢?
下面我们看看在linux下的运行结果:
对比发现:我们类中的自定义类型中含有指针,且自定义类型中没有显示构造,而类中有内置类型的显示构造时,vs会把自定义类型也进行初始化,而在Linux下则不会,这种属于编译器的问题!!!
析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
class STU
{
public:
STU()
{
cout << "构造函数:STU()" << endl;
}
~STU()
{
cout << "析构函数:~STU()" << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
int main()
{
STU stu1;
return 0;
}
我们可以看到析构函数和我们的构造函数一样是默认调用的。
我们可以把释放申请的空间在析构函数实现,这样可以有效的避免我们的内存泄漏。
析构函数的特性如下:
class PEO
{
public:
~PEO()
{
cout << "析构函数:~PEO()" << endl;
}
int _age;//年龄
int _height;//身高
int* _tmp;
};
class STU
{
public:
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
PEO peo;
};
int main()
{
STU stu1;
return 0;
}
我们在main方法中根本没有直接创建PEO类的对象,为什么最后会调用PEO类的析构函数?
原因:main方法中创建了STU对象stu1,而d中包含4个成员变量,其中__math, _chinese,_english三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而peo是PEO类对象,所以在stu1销毁时,要将其内部包含的PEO类的peo对象销毁,所以要调用PEO类的析构函数。
但是:main函数中不能直接调用PEO类的析构函数,实际要释放的是STU类对象,所以编译器会调用STU类的析构函数,而STU没有显式提供,则编译器会给STU类生成一个默认的析构函数,目的是在其内部调用PEO类的析构函数,即当STU对象销毁时,要保证其内部每个自定义对象都可以正确销毁main函数中并没有直接调用PEO类析构函数,而是显式调用编译器为STU类生成的默认析构函数。
注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。
总结一句话就是,析构函数和构造函数一样,对内置类型不做处理,对自定义类型调用相应的析构函数。
拷贝构造函数: 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
class STU
{
public:
STU(int math = 0, int chinese = 0, int english = 0)
{
_math = math;
_chinese = chinese;
_english = english;
}
STU(STU& stu)//拷贝构造函数
{
_math = stu._math;
_chinese = stu._chinese;
_english = stu._english;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
int main()
{
STU stu1(100,100,100);
STU stu2(stu1);
stu1.Show();
stu2.Show();
return 0;
}
拷贝构造的特性如下:
class STU
{
public:
STU()//构造函数
{}
STU(STU& stu)//拷贝构造函数
{
cout << "拷贝构造函数:STU()" << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
void Fun(STU stu)
{
cout << "普通函数:Fun()" << endl;
}
int main()
{
STU stu1;
Fun(stu1);
return 0;
}
当我们调用普通函数时,因为是传值调用,所以会对我们的实参进行拷贝,调用拷贝构造,如果我们的拷贝构造还是传值调用,则拷贝构造在调用拷贝构造,一直递归下去。所以我们的拷贝构造要使用引用。
class STU
{
public:
STU()//构造函数
{
tmp = (int*)malloc(sizeof(int));
}
STU(STU& stu)//拷贝构造函数
{
cout << "拷贝构造函数:STU()" << endl;
}
~STU()//构造函数
{
free(tmp);
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
int* tmp;
};
int main()
{
STU stu1;
STU stu2(stu1);
return 0;
}
此时进行的是浅拷贝。下面通过调试让我们看看浅拷贝的面目吧:
我们可以看到stu1的tmp地址和stu2的tmp地址相同,证明这两块地址指向同一块空间,当我们进行空间释放时,会对同一块空间进行释放。进而造成程序崩溃,解决问题的方法是进行深拷贝。
我们一般在函数返回值类型为类类型对象,使用已存在对象创建新对象或者函数参数类型为类类型对象使用我们的拷贝构造。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
注意:
不能通过连接其他符号来创建新的操作符:比如operator@。
重载操作符必须有一个类类型参数。
用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
.* :: sizeof ?: . 这5个运算符不能重载。
重载我们在下一章着重讨论,今天就看我们的赋值重载。
赋值运算符重载格式:
参数类型:const T&,传递引用可以提高传参效率
返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
检测是否自己给自己赋值
返回*this :要复合连续赋值的含义
class STU
{
public:
STU(int math = 0, int chinese = 0, int english = 0)
{
_math = math;
_chinese = chinese;
_english = english;
}
STU& operator=(const STU& stu)
{
if (this != &stu)
{
_math = stu._math;
_chinese = stu._chinese;
_english = stu._english;
}
return *this;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
int main()
{
STU stu1(100,100,100);
STU stu2;
stu2 = stu1;
stu1.Show();
stu2.Show();
return 0;
}
赋值运算符只能重载成类的成员函数不能重载成全局函数。
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
当我们没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。
class STU
{
public:
STU(int math = 0, int chinese = 0, int english = 0)
{
_math = math;
_chinese = chinese;
_english = english;
}
void Show()
{
cout << "数学:" << _math << " 语文:" << _chinese << " 英语:" << _english << endl;
}
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
int main()
{
STU stu1(100,100,100);
STU stu2;
stu2 = stu1;
stu1.Show();
stu2.Show();
return 0;
}
同样我们不能对需要开辟空间的地方直接使用默认的赋值重载,否则还是两个指针指向同一块空间。
当我们需要析构函数时也需要拷贝和赋值操作。
原因:内置类型系统会自动会收,我们需要在析构函数中释放我们申请的内存,如果我们使用默认的拷贝和赋值,那么进行的就是浅拷贝行为,在我们析构时程序会崩溃。
如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义拷贝赋值运算符和拷贝构造函数。
如果一个类需要一个拷贝构造函数时,几乎可以肯定它也需要一个拷贝赋值运算符。反之亦然,当我们的类需要拷贝赋值运算符时一定需要拷贝构造函数时。然而,无论是需要拷贝构造函数还是拷贝赋值运算符都不必然意味着需要析构函数。
在一些情况下我们不想让类默认生成这些函数,我们可以定义删除函数。通过在后面加上=delete来指出我们希望定义删除的函数。
class STU
{
public:
STU() = default;//使用默认的构造函数
STU(STU&stu) = delete;//阻止拷贝
~STU() = default;//使用默认的析构函数
private:
//成员变量
int _math;//学生数学成绩
int _chinese;//学生语文成绩
int _english;//学生英语成绩
};
析构函数不能是删除的成员。
我们类的已将入门了,下一篇我们会对类进行进一步的学习。