文章目录
标题:类和对象概念及实现详解(上篇)
作者:@Ggggggtm
寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景
一、什么是类和对象呢?
1、类的引入
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如: 之前在数据结构初阶中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现, 会发现struct中也可以定义函数。
2、类的定义
class className
{
// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
类的两种定义方式:
声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::。
3、类的访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符说明:
- public修饰的成员在类外可以直接被访问;
- protected和private修饰的成员在类外不能直接被访问;
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止;
- 如果后面没有访问限定符,作用域就到 } 即类结束;
- class的默认访问权限为private,struct为public(因为struct要兼容C) 。
4、类对象的储存方式
我们假想:每个对象中成员变量是不同的,但是调用同一份成员函数,如果按照每实例一个对象都给成员变量和成员函数创造一次空间存储,当一 个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
针对上面的问题,类的存储就变成了:只保存成员变量,成员函数存放在公共的代码段 。那么一个类的大小其实就是:实际就是该类中”成员变量”之和,当然要注意内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
5、this指针的特性
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout <<_year<< "-" <<_month << "-"<< _day <<endl;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1, d2;
d1.Init(2022,1,11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
我们知道了成员函数是放在了公共代码段。函数体中没有关于不同对象的区分。那么在上面的代码中d1和d2同时掉用了Print()函数,怎么是分别打印出d1对象中的成员变量和d2对象中的成员变量呢?(当然Init函数与Print函数的区分类似)。
C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。如下图:
this指针的特性:
this指针的类型:类类型* const,即成员函数中,不能给this指针赋值; this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递; this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针; 只能在“成员函数”的內部使用。
二、类的六个默认成员函数详解
什么是默认成员函数呢?
默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
如果一个类中什么成员都没有,简称为空类。 空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数:
- 构造函数;
- 析构函数;
- 拷贝构造;
- 赋值重载;
- 普通对象取地址;
- const对象取地址。
我们来看一下各个默认的成员函数的概念及实现。本篇我们先掌握构造函数和析构函数,这两个时相对较为麻烦和重要的,下篇我们会接着是西安剩余的默认成员函数以及类和对象剩余的重要的部分。
1、构造函数
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。其特征如下:
- 函数名与类名相同;
- 无返回值;
- 对象实例化时自动调用对应的构造函数;
- 构造函数可以重载。
- 如果如果类中没有显式定义构造函数,则C++编译器会自动生成一个默认的无参构造函数,一但用户显式定义编译器将不再生成。
我们结合着以下代码一起理解以下。
class Date
{
public:
// 1.无参构造函数
Date()
{}
// 2.带参构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
void TestDate()
{
Date d1; // 调用无参构造函数
Date d2(2015, 1, 1); // 调用带参的构造函数
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
Date d3();
}
关于编译器生成的默认成员函数,很多人都会有疑惑:不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?调用了编译器生成的默认构造函数,但是对象中的成员函数依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用??C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。其实编译器生成默认的构造函数会对自定类型成员调用的它的默认成员函数,并不会对内置类型的成员变量进行初始化。如下面代码:Data中并没有自己实现构造函数,系统会自动生成。让后会对自定义类型成员_t调用它的默认成员函数。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。
2、析构函数
通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?这里就用到了析构函数。析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。
析构函数是特殊的成员函数,其特征如下:
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型;
一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载; 对象生命周期结束时,C++编译系统系统自动调用析构函数。我们结合下面代码一起理解一下。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(DataType data)
{
//CheckCapacity();
_array[_size] = data;
_size++;
}
// 其他方法...
~Stack()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
void TestStack()
{
Stack s;
s.Push(1);
s.Push(2);
}
关于编译器自动生成的析构函数,是否会完成一些事情呢?编译器生成的默认析构函数,对自定类型成员调用它的析构函数。注意:创建那个类的对象则调用该类的析构函数,销毁哪个类的对象则调用该类的析构函数。如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,有资源申请时,一定要写,否则会造成资源泄漏。
3、未完待续……
今天的内容就到这里,需要重点理解和掌握构造函数和析构函数,后续我会更新下篇带大家理解完类和对象ovo!
希望以上内容对你有所帮助,感谢阅读。