类和对象(上)
前一章节,我们浅浅提到了类和对象的一些概念,现在,我们一起来回顾一下.
类的定义
类定义格式
class为定义类关键字,后面跟上定义类的名字,{}为类的主体,后面要接";",定义在类的成员函数默认展开(inline)
访问限定符
一种封装格式,分为public, private, protected三种,访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到 }即类结束。
类域
类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作⽤域操作符指明成员属于哪个类域。
以下为正片 ///
实例化
概念
• ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
• 类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只
是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
• ⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。
总结出一句话来说就是:用类实例化对象才会分配空间.(就像用图纸造出房子才会占用空间一样)
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;
};
上面的代码中,在private中的成员变量只是被定义,并没有开辟新的空间;而
// Date类实例化出对象d1和d2
Date d1;
Date d2;
则是实例化了两个新对象,所以开辟了新空间.
内存对齐规则
定义
• 第⼀个成员在与结构体偏移量为0的地址处。
• 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。
• 注意:对⻬数 = 编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。
• VS中默认的对⻬数为8
• 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。
• 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩
就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。
这是我们在C语言时就了解的知识,以下题为例:
// 计算⼀下A实例化的对象是多⼤?
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
没有调用函数,所以直接看成员变量即可. _ch是char字符类型,大小1字节;_i是int整型类型,大小4字节,取所有成员中占据最大的空间为对齐数,所以是_ch放在下标为0的位置上,空三格,再放_i, 总共占据了0~7这8个字节的空间,是最大对齐数的整数倍,满足条件,所以这题答案是8.
而如果是这两题呢?
class B
{
public:
void Print()
{
//...
}
};
class C
{
};
我们可以发现,B类中有一个空函数,C类中没有成员,在C语言中,我们将这两类都令其占用1个字节的空间.
this指针
概念
• Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和
Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了
⼀个隐含的this指针解决这⾥的问题
• 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。⽐如Date类的Init的真实原型为, void Init(Date* const this, int year,
int month, int day)
• 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this-
>_year = year;
• C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显不使⽤this指针。
举个例子
class Date
{
public:
// void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day)
{
// 编译报错:error C2106: “=”: 左操作数必须为左值
// this = nullptr;
// this->_year = year;
_year = year;
this->_month = month;
this->_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
// 这⾥只是声明,没有开空间
int _year;
int _month;
int _day;
};
int main()
{
// Date类实例化出对象d1和d2
Date d1;
Date d2;
// d1.Init(&d1, 2024, 3, 31);this指针
d1.Init(2024, 3, 31);
// d1.Init(&d2, 2024, 7, 5);
d2.Init(2024, 7, 5);
//不能自己写
return 0;
}
对于Init函数中,实参的写入,C++都会在函数的第一个变量中加入一个看不见的常量this指针(无法被修改),如函数中的注释所示.
注意:
只要空指针没有被解引用就不会报错
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
尽管这里将p赋值为一个空指针,并将其指向Print函数,但并没有将其解引用,所以这里不会报错.而这一个程序:
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;//这个地方多了一句
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
与上一个程序相比,只多了一句cout << _a << endl,但此时空指针指向了一个成员变量,此时会对空指针进行解引用,故程序会报错.
类的默认成员函数
概念
默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,分别是以下6个
构造函数
定义
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。其实质是想要替换初始化Init函数.
特点
前五个特点
1. 函数名与类名相同。
2. ⽆返回值。 (无需写void)
3. 对象实例化时系统会⾃动调⽤对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤户显式定义编译器将不再⽣成。
前五项较为简单,我们来举个例子:
class Date
{
public:
//1.⽆参构造函数,无需写void
Date()//若不写,则构造函数会写出这一中无参构造函数
{
_year = 1;
_month = 1;
_day = 1;
}
//2.全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d1;//此时相当于已经自动调用了Init函数
Date d2;
return 0;
}
程序中写出了两个构造函数,一个是无参构造函数,一个是全缺省构造函数. 当我们不写函时,程序会最大写一个无参构造函数. 除此之外,前面我们说过,这两个函数虽然是重载函数,但是由于调用歧义,两个函数并不能同时存在.
后两个特点
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。
6
第六个特点较为复杂的地方是:⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数,且只能存在一个. 千万不能将默认构造函数理解为编译器默认⽣成的构造函数!
7
7. 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
析构函数
概念
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的,所以析构函数像Destroy函数一样,只应该对需要非局部对象如要申请资源的数据结构例如栈、队列...使用.
特点
1. 析构函数名是在类名前加上字符 ~。(与C语言的按位取反相似)
2. ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
8. ⼀个局部域的多个对象,C++规定后定义的先析构。
析构函数的使用
同上面的构造函数,C++自动生成的默认析构函数一般不会对我们有很大的帮助,但在一些少数情况下比较方便,例如用两个栈实现队列:
public:
Stack(int n = 4)//相当于Init
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc fail!");
return;
}
_capacity = n;
_top = 0;
}
~Stack()//相当于Destroy
{
cout << "~Stack()" << endl;
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
class MyQueue
{
public:
//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
// 显⽰写析构,也会⾃动调⽤Stack的析构
private:
Stack pushst;
Stack popst;
};
此时MyQueue中编译器自动调用了栈的构造函数(初始化)和析构函数(销毁).
原文地址:https://blog.csdn.net/Frenemy__/article/details/140363640
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!