四、类和对象
1. 类的定义
通俗的理解,类其实就是一个模子,是一个变量类型,对象就是这个类型定义出来的具体的变量,就像int a;这句话,int对应类,a就对应对象。
概括的讲:类是对象的抽象和概括,而对象是类的具体和实例。
那么C++中的类长什么样呢?简单说, C++中类的其实就是包含函数的结构体!因为C++类里面的成员除了可以像C语言的结构体那样包含基本变量以外,还可以包含函数,前者叫做成员变量,后者叫做成员方法。
关键字用 class/struct 类定义,比如下面定义一个C++的类,学生类:
class Student
{
public:
int num;
char name[100];
int score;
int print()
{
cout<<num<<" "<<name<<" "<<score;
return 0;
}
}; // 注意有分号
形式上和C语言的结构体非常像,成员有变量也有函数,今后要习惯称之为属性和方法了。
类里还有一个public的东西,它是控制成员访问权限的一个存取控制属性,除了public以外,还有private、protected一共三种,
- 其中private表示私有,被它声明的成员,仅仅能被该类里的成员访问,外界不能访问,是最封闭的一种权限;
- protected比private稍微公开一些,除了类内自己的成员可以访问外,它的子类也可以访问
- 而public声明的成员,则可以被该类的任何对象访问,是完全公开的数据。
如果类里的成员函数很多的话,阅读起来就会乱很多,故此,C++还支持另外一种写法,就是成员函数仅在类内声明函数原型,在类外定义函数体,这样在类里可以看到所有成员函数的列表,像目录一样一目了然,规范很多。
class Student
{
public:
int num;//学号
char name[100];//名字
int score;//成绩
int print();//类内声明print函数
};
int Student::print()//在类外定义完整的print函数
{
cout<<num<<" "<<name<<" "<<score;
return 0;
}
2. 对象的建立和使用
1、对象的创建
类就是包含函数的结构体,是一种自定义数据类型,用它定义出来变量,就是对象,这就是所谓的“对象是类的具体和实例”,定义了一个这个类的对象,也可以说实例化了一个对象,就是这个意思!
对象的使用,和结构体的使用也一样,都是主要访问里面的成员,也都是用过.的方式来访问,如:
Student A;
A.num = 101;
strcpy(A.name,"dotcpp");
A.score = 100;
A.print();
需要注意的是,这里类中的成员变量都是声明为public类型的,如果声明为private类型,则在主函数中主要通过对象.变量
的方式直接访问的话就会被禁止报错,原因private类型的变量是私有类型,不允许外部访问。
对于想保护但又想控制的私有变量,我们通常将其声明为private类型,然后同时定义一个public类型的专门赋值的方法,由于内部成员可以访问private声明的变量,我们就可以在外部通过这个public的方法来间接控制这些私有的成员,来起到封装、保护的效果,而这个public类型的方法,也称之为这个类的一个外部接口!
2、对象的指针
与普通变量一样,对象也是一片连续的内存空间,因此也可以创建一个指向对象的指针,即对象指针,存储这个对象的地址。
定义方法如下:
类名 *指针名;
如定义Student *p;
定义一个Clock类型的指针p,需要清楚的是,这里并没有建立对象,当然也不会调用构造函数。
接下来就可以将一个同类型的类对象地址赋值给这个指针,然后通过->来访问对象中的成员。
Student *p;
Student A;
p = &A;
p->print();
除了在赋值、访问成员的时候用以外,在传参的时候也建议用指针来传递,因为其传递的为地址,不会进行对象之间的副本赋值,从而减少内存的开销,提高效率。
3、对象的引用
引用,是C++中一种新的类型,对象引用就是一个类对象起个别名,本质上也是把这个类对象的地址赋给了这个引用类型,两者是指向一块内存空间的。
Student A;
Student &Aq=A;
如以上代码,定义一个Student类型的对象,然后用&来定义一个该类类型的引用类型,并把A对象赋给Aq作为初始化。
注意!
- 与指针一样,两者必须是同类型才可以引用。
- 除非做函数的返回值或形参时,其余定义引用类型的同时就要初始化!
- 引用类型并非是新建立一个对象,因此也不会调用构造函数。
既然是类对象的别名,因此使用方法也和类对象一样,用别名.成员的方法进行访问,如:
Student A;
Student &Aq=A;
Aq.print();
引用类型的优点:
可以看到,用引用类型时,本质还是存的地址,因此无论传参定义都不会太多内存开销,有指针的优势,同时使用起来和类对象本身使用一样,在做函数实参时,直接传入引用对象就可以,不用加地址符,因此看起来更直观、方便。
3. 构造函数constructor
一种特殊的函数,它在类里,与类名同名,且没有返回值。只要我们定义一个类的对象,系统就会自动调用它,进行专门的初始化对象用,而大多数情况下,因为我们没有定义构造函数,系统会默认生成一个默认形式、隐藏着的构造函数,这个构造函数的函数体是空着的,因此不具有任何功能。
如何定义自己的构造函数?用户自行定义了至少一个构造函数,系统就不在自动生成,而是根据用户定义的构造函数选择最匹配的一个进行调用。
#include<iostream>
#include<cstring>
using namespace std;
class Student {
public:
Student(int n,char *str,int s);
int print();
int Set(int n,char *str,int s);
private:
int num;//学号
char name[100];//名字
int score;//成绩
};
Student::Student(int n,char *str,int s) {
num = n;
strcpy(name,str);
score = s;
cout<<"Constructor"<<endl;
}
int Student::print() {
cout<<num<<" "<<name<<" "<<score;
return 0;
}
int Student::Set(int n,char *str,int s) {
num = n;
strcpy(name,str);
score = s;
}
int main() {
Student A(100,"dotcpp",11);
A.print();
return 0;
}
4. 析构函数destructor
类对象在创建时自动调用的构造函数,在对象销毁时也会自动调用一个函数,它也和类名同名,也没有返回值,名字前有一个个波浪线~,用来区分构造函数,它的作用主要是用做对象释放后的清理善后工作。它,就是析构函数。
与构造函数相同的是,与类名相同,没有返回值,如果用户不定义,系统也会自动生成一个空的析构函数。而一旦用户定义,则对象在销毁时自动调用。
与构造函数不同的是,虽然他俩都为公开类型。构造可以重载,有多个兄弟,而析构却不能重载,但它可以是虚函数,一个类只能有一个析构函数。
#include<iostream>
#include<cstring>
using namespace std;
class Student {
public:
Student(int n,char *str,int s);
~Student();
int print();
int Set(int n,char *str,int s);
private:
int num;//学号
char name[100];//名字
int score;//成绩
};
Student::Student(int n,char *str,int s) {
num = n;
strcpy(name,str);
score = s;
cout << num << " " << name << " " << score << " ";
cout<<"Constructor"<<endl;
}
Student::~Student() {
cout << num << " " << name << " " << score << " ";
cout << "destructor" << endl;
}
int Student::print() {
cout<<num<<" "<<name<<" "<<score;
return 0;
}
int Student::Set(int n,char *str,int s) {
num = n;
strcpy(name,str);
score = s;
}
int main() {
Student A(100,"dotcpp",11);
Student B(101, "cpp", 12);
return 0;
}
可以看到对象A和B的构造函数的调用顺序以及构造函数的调用顺序,完全相反!原因在于A和B对象同属局部对象,也在栈区存储,也遵循“先进后出”的顺序!
5. 拷贝构造函数
在C++中,与类名同名,且形参是本类对象的引用类型的函数,叫做拷贝构造函数(Copy Constrctor),与构造函数一样,当我们不主动定义的时候,系统也会自动生成一个,进行两个对象成员之间对应的简单赋值,用来初始化一个对象。
示例:
#include<iostream>
using namespace std;
#define PI 3.1415
class Circle
{
private:
double R;
public:
Circle(double R);
Circle(Circle &A);
double area();
double girth();
};
Circle::Circle(double R)
{
cout<<"Constructor"<<endl;
this->R = R;
}
Circle::Circle(Circle &A)
{
cout<<"Copy Constructor"<<endl;
this->R = A.R;
}
double Circle::area()
{
return PI*R*R;
}
double Circle::girth()
{
return 2*PI*R;
}
int main()
{
Circle A(5);
Circle B(A);
return 0;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nkcbHZ1h-1634891964069)(https://gitee.com/ClimberCoding/picturebed/raw/master/img/image-20211022162129922.png)]
一个额外的示例:
#include <iostream>
using namespace std;
class Line {
public:
int getLength(void);
Line(int len); // 简单的构造函数
Line(const Line& obj); // 拷贝构造函数
~Line(); // 析构函数
private:
int* ptr;
};
// 成员函数定义
Line::Line(int len) {
cout << "调用构造函数" << endl;
// 为指针分配内存
ptr = new int;
*ptr = len;
}
Line::Line(const Line& obj) {
cout << "调用拷贝构造函数,并为指针ptr分配内存" << endl;
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}
Line::~Line(void) {
cout << "释放内存" << endl;
delete ptr;
}
int Line::getLength(void) {
return *ptr;
}
void display(Line obj) {
cout << "line大小:" << obj.getLength() << endl;
}
int main() {
Line line{10};
Line line2 = line; // 这里也用了拷贝构造函数
display(line);
display(line2);
return 0;
}
默认的拷贝构造函数仅仅是做简单的赋值,有些情况则要出现问题,这就涉及到深拷贝与浅拷贝
6. 浅拷贝与深拷贝
刚才的Circle类中,如果成员变量中加一个指针成员,初始化中需要动态开辟内存,则会出现极大的安全隐患,代码如下:
#include <iostream>
#include <cstring>
using namespace std;
#define PI 3.1415
class Circle
{
private:
double R;
char* str;
public:
Circle(double R, char* str);
//Circle(Circle &A);
~Circle();
double area();
double girth();
};
Circle::Circle(double R, char* str)
{
cout<<"Constructor"<<endl;
this->R = R;
this->str = new char[strlen(str) + 1];
strcpy(this->str, str);
cout << this->R << " " << this->str << endl;
}
// Circle::Circle(Circle &A)
// {
// cout<<"Copy Constructor"<<endl;
// this->R = A.R;
// }
Circle::~Circle() {
delete[] str;
}
double Circle::area()
{
return PI*R*R;
}
double Circle::girth()
{
return 2*PI*R;
}
int main()
{
Circle A(5, "NO.1 Old class");
Circle B(A);
return 0;
}
可以看出报错了,原因在于,默认的拷贝构造函数仅仅是进行数据赋值,并不能为指针开辟内存空间,相当于代码:This->str = str;
那么本质上,也就是两个指针指向一块堆空间。已经违背了我们的初衷。那么在程序结束的时候,两个对象回收的时候,会调用自己的析构函数,释放这块内存空间,由于两个对象要调用两次,即delete两次,就会出现错误!
所以,当类中有指针类型时,依靠默认的拷贝构造函数的方法,已经无法满足我们的需求,必须定义一个特定的拷贝构造函数,即不仅可以进行数据的拷贝,也可以为成员分配内存空间,实现真正的拷贝,也叫做深拷贝,这就是深拷贝构造函数。
#include <iostream>
#include <cstring>
using namespace std;
#define PI 3.1415
class Circle
{
private:
double R;
char* str;
public:
Circle(double R, char* str);
Circle(Circle &A);
~Circle();
double area();
double girth();
};
Circle::Circle(double R, char* str)
{
cout<<"Constructor"<<endl;
this->R = R;
this->str = new char[strlen(str) + 1];
strcpy(this->str, str);
}
Circle::Circle(Circle &A)
{
cout<<"Copy Constructor"<<endl;
this->R = A.R;
this->str = new char[strlen(A.str) + 1];
strcpy(this->str, A.str);
}
Circle::~Circle() {
delete[] str;
cout << "call destructor" << endl;
}
double Circle::area()
{
return PI*R*R;
}
double Circle::girth()
{
return 2*PI*R;
}
int main()
{
Circle A(5, "NO.1 Old class");
Circle B(A);
return 0;
}
其实现原理与带参数的构造函数类似,在赋值之前开辟足够的内存空间,来真正完成完整的拷贝,这就是所谓的“深拷贝”。
7. this指针
一个类当中,有一个很隐蔽的特殊指针,它就是—this指针!
为什么说它特殊?因为只要定义一个类,系统就会预定义个名字叫做this名且指向当前对象的指针。虽然我们看不到但却可以使用它。
如,来看一个时钟类的一个成员函数,用来设置时间传值的代码
int Clock::SetTime(int h,int m,int s) {
H = h;
M = m;
S = s;
}
如果我们知道了this的存在,就可以这样写:
int Clock::SetTime(int h,int m,int s)
{
this->H = h;
this->M = m;
this->S = s;
}
//也可以写成:
int Clock::SetTime(int h,int m,int s)
{
(*this).H = h;
(*this).M= m;
(*this).S= s;
}
可以看到,以上两种写法用到了对象中的隐藏的this指针,可以明确是本类中的成员,从而明显的区别本对象与外部变量。
实际上,当一个对象调用其成员函数的时候,即便程序中有多个该类的对象,但成员函数的代码也仅有一份,所以为了区分它们是哪个对象调用的成员函数,编译器也是转化成this->成员函数这种形式来使用的。
8. 友元函数
我们都知道类中的私有成员,只有被类里的成员函数访问,在类外是不能访问的。这体现了C++中类设计的封装、隐蔽思想。是C++最基本的优点。
但如果偶尔有的时候,我们在类外又确实想访问这些私有成员,就会变得麻烦很多,就处于既访问不到又不能声明为public类型的两难处境。
而友元的出现就可以很好的解决这个问题。即把外部的函数声明为友元类型,赋予它可以访问类内私有成员的权利。来做到两全其美。这就是友元的意义,从字面意思也可以看出来,像“朋友”一样,开了一个绿色通道。
友元的对象,它可以是全局的一般函数,也可以是其他类里的成员函数,这种叫做友元函数。不仅如此,友元还可以是一个类,这种叫做友元类。
来看怎么在C++中使用friend
对于友元函数,只需要在类内对这个函数进行声明,并在之前加上friend关键字。这个函数就具有了独特的权限,成为友元函数。
注意,友元并不属于这个类本身,无论是友元函数还是友元类。都不能使用类内的this指针,同时也不可以被继承,如同父亲的朋友不一定是儿子的朋友这个道理。
#include<iostream>
#include<math.h>
using namespace std;
class Point
{
private:
double x;
double y;
public:
Point(double a,double b)
{
x = a;
y = b;
}
int GetPoint()
{
cout<<"("<<x<<","<<y<<")";
return 0;
}
friend double Distance(Point &a,Point &b);
};
//求两点之间的距离
double Distance(Point &a,Point &b)
{
double xx;
double yy;
xx = a.x-b.x;
yy = a.y-b.y;
return sqrt(xx*xx+yy*yy);
}
int main()
{
Point A(2.0,3.0);
Point B(1.0,2.0);
double dis;
dis = Distance(A,B);
cout<<dis<<endl;
return 0;
}
可以看到实现求两点之间距离的函数为外部的一般函数,由于需要访问类内的私有成员,所以我们把它在类内声明成frined友元类型。
注意,作为friend的函数Distance一定要在类Point之后定义。
9. 友元类
如果把一个类A声明为另一个类B的友元类,则类A中的所有成员函数都可以访问B类中的成员。使用方法也一样,在类B中进行声明即可。
#include<iostream>
#include<math.h>
using namespace std;
class Point
{
private:
double x;
double y;
public:
Point(double a,double b)
{
x = a;
y = b;
}
int GetPoint()
{
cout<<"("<<x<<","<<y<<")";
return 0;
}
int distancetoLine()
{
}
friend class Tool;
};
class Tool
{
public:
double GetX(Point &A)
{
cout<<A.x<<endl;
return A.x;
}
double GetY(Point &A)
{
cout<<A.y<<endl;
return A.y;
}
double dis(Point &A)
{
cout<<sqrt(A.x*A.x+A.y*A.y)<<endl;
return sqrt(A.x*A.x+A.y*A.y);
}
};
int main()
{
Point A(2.0,3.0);
Tool T;
T.GetX(A);
T.GetX(A);
T.dis(A);
return 0;
}
一定要注意,上面作为friend的类A,一定要在类B之后定义。
最后我们总结友元机制的优缺点总结如下:
优点:更方便快捷的访问类内的私有成员
缺点:打破了C++中的封装思想
10. C++中常数据的使用及初始化
常的概念在学C语言时候就有了解,关键字是const,所谓的“常”,或或者说被“常”修饰的变量,是不可修改、被改变的,比如用const修饰的一个变量就成了常变量,这个值不可被更改。
那么C++中,一样有常的概念,额外不同的是,const除了可以修饰一般的变量为常变量之外,还可用于修饰某个对象,变成常对象。以及可以修饰类的数据成员和成员函数,分别叫做类的常数据成员和常成员函数。
1、常数据成员
格式:
数据类型 const 数据成员名;
const 数据类型 数据成员名;
被const修饰的成员则必须进行初始化,并且不能被更改。 而初始化的方式则是在类的构造函数的初始化列表里进行的。
特殊情况,如果成员是static类型,即静态常数据成员,因为是静态的属性,初始化则需在类外进行初始化,即便该数据成员为私有类型!
#include<iostream>
using namespace std;
class Clock
{
private:
const int h; //修饰h为常类型成员
const int m; //修饰m为常类型成员
int const s; //和上面两种用法都可以
static const int x;
public:
Clock(int a,int b,int c):h(a),m(b),s(c)
{
cout<<"Constrctor! Called"<<endl;
}
int ShowTime()
{
cout<<h<<":"<<m<<":"<<s<<endl;
return 0;
}
int GetX()
{
cout<<x<<endl;
return 0;
}
};
const int Clock::x = 99; // 类外初始化
int main()
{
Clock A(12,10,30);
A.ShowTime();
A.GetX();
return 0;
}
2、常对象
C++中可以把一个对象声明为const类型,即常对象。这样声明之后,这个对象在整个生命周期中就不可以再被更改,所以在定义的时候要由构造函数进行初始化。
格式:
类型 const 对象名;
const 类型 对象名;
注意,常对象不可以访问类中的非常成员函数,只能访问常成员函数。
成员变量不管是不是常量,常对象都可以访问,但是不能修改。
#include<iostream>
using namespace std;
class Clock
{
private:
const int h; //修饰h为常类型成员
const int m; //修饰m为常类型成员
int const s; //和上面两种用法都可以
int x;
public:
Clock(int a,int b,int c):h(a),m(b),s(c)
{
x=99;
cout<<"Constrctor! Called"<<endl;
}
int ShowTime()
{
cout<<h<<":"<<m<<":"<<s<<endl;
return 0;
}
int GetX() const
{
//x=99;
cout<<x<<endl;
return 0;
}
int a = 12;
};
int main()
{
const Clock A(12,10,30);
const Clock B(14,20,50);
//A = B; // A为常对象不可以被赋值
//A.ShowTime(); // 常对象无法访问非常成员函数
A.GetX();
A.a; // 常对象可以访问非常成员变量
return 0;
}
注意看上面注释的两行,正常编译将报错
3、常成员函数
一个类中的成员函数被const修饰后,就变成了常成员函数,常成员函数的定义如下:
返回类型 函数名(形参表列)const;
注意:
- 常成员函数的定义和声明部分都要包含const;
- 常成员函数只能调用常成员函数,而不能调用非常成员函数,访问但不可以更改非常成员变量。
#include<iostream>
using namespace std;
class Clock
{
private:
const int h; //修饰h为常类型成员
const int m; //修饰m为常类型成员
int const s; //和上面两种用法都可以
int x;
public:
Clock(int a,int b,int c):h(a),m(b),s(c)
{
x=99;
cout<<"Constrctor! Called"<<endl;
}
int ShowTime()
{
cout<<h<<":"<<m<<":"<<s<<endl;
return 0;
}
int GetX() const
{
//x=99;
cout<<x<<endl; // 注意的第二条
return 0;
}
};
int main()
{
const Clock A(12,10,30);
A.GetX();
return 0;
}
C++中常概念的建立,明确了程序中各对象的变与不变的边界,也加强了C++程序的安全性和可控性。
总结一下:
常数据成员:若有特殊的static常数据成员,就要在类外初始化,其余的都在构造函数列表中初始化。
常对象:只能访问常成员函数(函数后面加const的);数据成员不管是不是常量都能访问,但是不能修改。
常成员函数:在本函数中只能调用常成员函数,数据成员不管是不是常量都能访问,但是不能修改。