【C++笔记】类和对象(上)
前言
各位读者朋友的大家好,上期我们讲完了C++的引用、inline关键字以及nullptr,这期我们开启新的部分—类和对象,下面开始讲解。
目录
- 前言
- 一. 类的定义
- 1. 类定义格式
- 2. 访问限定符
- 3. 类定义的注意事项
- 4. 类域
- 二. 类的实例化
- 1. 实例化的概念
- 2. 对象的大小
- 3. 内存对齐
- 三. this指针
- 四. C++和C语言实现Stack对比
- 结语
一. 类的定义
1. 类定义格式
- class是定义类的关键字,Stack是类的名字,{}内是类的主体,要注意类定义结束时花括号后面分号不能省略。类主体中的内容称为类的成员:类中的变量称为类的属性或成员变量(这里的成员变量只是声明,没有开空间);类中的函数称为类的方法或成员函数。
2. 访问限定符
- C++一种实现封装的方式,用类将对象的属性(变量)和方法(函数)结合在一起,让对象更加完善,通过访问权限的选择性的将其接口提供给外部的用户使用。
- public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,现阶段我们认为protected和private是一样的,在以后的继承章节才能体现出他们的区别。
如图,我们可以直接访问public修饰的成员函数而不能访问private修饰的成员变量。
特别需要注意的是,在C++中类的名字就是类的类型,就不需要像C语言中结构体那样为了类型名短一些typedef一下使用了。 - 访问权限作用域从访问限定符出现的位置开始直到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就到},即类结束。
- class定义成员没有访问限定符修饰时默认为private(私有),struct默认为public(公有)。
class Yuey
{
int Add(int a, int b)
{
return a + b;
}
public:
int Mid(int a, int b)
{
return a * b;
}
private:
int a;
int b;
};
int main()
{
Yuey Yy;
Yy.Mid(1, 2);
return 0;
}
我们可以直接访问public限制的Mid函数,但不能访问未被修饰的Add函数。但是我们用struct作为类的关键字时,未被修饰的成员也能直接访问。
在C++语法中,struct升级成了类,类名就是类型,也可以typedef重新定义类型,在struct中也可以定义成员函数,C++中也兼容了C语言结构体的用法。一般情况下我们还是推荐用class定义类。
- 一般的成员变量会被限制为private/protected,需要给别人使用的成员函数会放到public。
3. 类定义的注意事项
- 为了区别成员变量,一般习惯上成员变量会加上一个特殊的标识,如成员变量的前面或后面加_或者以m开头,但C++中这个并不是强制的,只是惯例。
我们在类的成员函数中,成员变量都是year,month,day,参数也是year,month,day为了区分这两者,我们会在成员变量前面加_。 - 定义在类里的成员函数默认为inline。
- 成员函数可以只在类域中声明,但是在类外定义的时候要指明类域。
4. 类域
- 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义成员时,需要用::作用域操作符指明成员属于哪个类域。
- 类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就会把Init当作全局函数,在编译时,找不到array等成员的声明,定义在哪里,就会报错。指定类域Stack,就知道了Init是成员函数,当前类域找不到的array成员,就会到类域中去找。
定义和声明分离时需要指明类域,分离之后就不是内联函数了。
二. 类的实例化
1. 实例化的概念
- 所谓类的实例化就是用类的类型在物理内存中创建对象的过程。
- 类是对象进行一种抽象描述,是一个一模一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时才会分配空间。
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,储存类的成员变量。
class Yuey
{
public:
int Add(int a, int b) {
return a + b;
}
//只是声明,没有开空间
int _x;
int _y;
};
int main() {
//用类实例化Yy1和Yy2
Yuey Yy1;
Yuey Yy2;
return 0;
}
2. 对象的大小
类实例化出的每一个对象,都有独立的数据空间,所以对象中肯定会包含成员变量,那么成员函数是否能包含呢?答案是不能的。
我们转到汇编指令看一下:
在执行函数的时候,调用了同一块地址。这是因为函数被编译后是一段指令,对象中没办法储存,这些指令储存在一个单独的区域(代码段)中,如果对象中非要储存的话只能是成员函数的指针。但是对象中也没有存储指针,因为我们Yuey实例化出两个对象,这两个对象都有各自独立的成员变量来存储各自的数据,但是两者的成员函数Add的指针却是一样的,储存在对象中就浪费了。如果用Yuey实例化100个对象,那么成员函数指针就重复储存100次,太浪费了。
那不存储函数指针怎么找到函数?
函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接时,就找到函数的地址,不是运行时找,只有动态多态是在运行时找,就需要储存地址,这个我们后续会讲解。
3. 内存对齐
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员大小的较小值,VS中的默认对齐数是8。
- 结构体的总大小为:最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍。
这一成员的大小为12字节 - 对于嵌套了结构体的情况,嵌套的结构体成员对齐到自己最大成员中最大对齐数的整数倍处,结构体的整体的大小就是所有最大对齐数(含嵌套的结构体中成员的最大对齐数)的整数倍。
为了节省空间,我们在设计结构体的时候可以将占用空间小的成员放在一起。
上面的类C和Y没有成员变量,但是将其实例化后,他们的大小是1字节,为什么呢?如果一个字节都不给,怎么表示对象存在呢!这里给一个字节单纯是为了占位标识对象存在。
三. this指针
#include<iostream>
using namespace std;
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;
Date d2;
d1.Init(2024, 10, 1);
d1.Print();
d2.Init(2024, 10, 13);
d2.Print();
return 0;
}
Date类中有Init和Print两个成员函数,函数体中是没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?这里C++给了一个隐含的this指针来解决这里的问题。上面的代码可以改写成这样:
- 类成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数给_year赋值。
- C++规定不能在实参和形参中将this指针显示的写出来,但是可以在函数体内显示的使用this指针。
this指针存放在内存的栈区,也有可能放在寄存器中(由编译器决定)。
下面看两个题: - 下面代码的运行结果:
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
- 相似的代码
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
四. C++和C语言实现Stack对比
- C语言实现栈
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
assert(ps);
// 满了, 扩容
if (ps->top == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
void STPop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
STDataType STTop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
return ps->a[ps->top - 1];
}
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
- C++实现栈
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
// 成员函数
void Init(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity *
sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
void Pop()
{
assert(_top > 0);
--_top;
}
bool Empty()
{
return _top == 0;
}
int Top()
{
assert(_top > 0);
return _a[_top - 1];
}
void Destroy()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
STDataType* _a;
size_t _capacity;
size_t _top;
};
通过上面两段代码对比,我们发现C++Stack形态上还是发生了很多变化的,逻辑和底层上没什么变化。
- C++中数据和函数封装到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种实现,这也是最重要的变化。这里的封装本质上是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,后续我们还会不断学习。
- C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含传递了,方便了很多,使用类型不再需要typedef,用类名就很方便。
- 在C++入门阶段实现的Stack看起来变化很多,实质上变化不大。等到后续STL中用适配器实现Stack,我们再来感受C++的魅力。
结语
以上我们就讲完了C++类和对象(上)的内容,感谢大家的阅读,欢迎大家批评指正,持续更新中。