【C++】类和对象(一)
目录
1 类的定义
1.1 类的定义格式
1.2 类域
2 实例化
2.1实例化的概念
2.2 对象
2.3 对象的大小
3 this指针
4 C++和C语言实现Stack对比
1 类的定义
1.1 类的定义格式
class为定义类的关键字,格式为class+类成员名字,然后{ }内部写代码,需要注意大括号后加分号,先看一段代码:
#include <iostream>
#include <assert.h>
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;
};
这段代码中Date为类的名字,{}中为类的主体,类中的变量称为类的属性或者成员变量,类中的函数称为类的方法或者成员函数。
为了区分成员变量,一般在成员变量加一个特殊标识,这里在成员变量前加一个_,这个不是唯一的标识,一些公司在成员变量后面加_
C++⼀种实现封装的方式,用类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限 选择性的将其接口提供给外部的用户使用。所以这里就提出一个访问限定符的概念,上述代码中public和private就是访问限定符。public修饰的成员可以在类的外面直接被访问,private和protected修饰的成员在类外面不能被访问,在这里private和protected是一样的,区别在后续写继承的时候会说明。
访问权限作用域从访问限定符开始到下一个访问限定符出现为止,如果后面没有访问限定符,作用域到}结束。
在C++中struct可以定义类,C++兼容C语言中struct的用法,定义类时将class替换成struct也是可以的。一般习惯用class定义类。这里注意一点,class定义成员没有被访问限定符修饰的,默认为private,struct默认为public。
1.2 类域
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
#include<iostream>
using namespace std;
class Stack
{
public://成员函数void Init(int n = 4);
private://成员变量int* array;size_t capacity;size_t top;
};
// 声明和定义分离,需要指定类域
void Stack::Init(int n)
{array = (int*)malloc(sizeof(int) * n);if (nullptr == array){perror("malloc申请空间失败");return;}capacity = n;top = 0;
}
类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明在哪里,就会报错。
2 实例化
2.1实例化的概念
用类的类型在物理内存中创建对象的过程,称为类实例化出对象。
打个比方,类就像房子的蓝图,对象就是实际的房子。
-
类 (Class):就像是房子的蓝图。
蓝图不是真正的房子,它定义了房子的规划:有几个卧室、几个卫生间。蓝图本身只是一个“模板”。 -
对象 (Object):就是根据蓝图建造出来的真正的房子。根据同一张蓝图,可以建造出无数个具体的房子。每个房子都是独立的,互不影响。
所以:类是用来创建对象的模板或蓝图,而对象是这个模板的一个具体实例。
下面看代码来感受一下:
// 这是一个类(蓝图)
class Stack {
private:// 以下是属性特征int* _data; int _top; int _capacity; public:// 以下是方法void Init(int capacity = 4) { // 初始化栈_data = new int[capacity];_capacity = capacity;_top = 0;}void Push(int x) { // 压栈操作// ... 检查容量等逻辑_data[_top++] = x;}void Pop() { // 出栈操作// ... 检查栈是否为空--_top;}int Top() { // 取栈顶元素return _data[_top - 1];}};
2.2 对象
对象是根据类这个蓝图创建出来的实实在在的变量。这个过程叫做“实例化”。具体解释请见注释。
int main() {// 以下是创建对象Stack s1; // 创建了第一个栈对象 s1 (根据蓝图建了第一套房)Stack s2; // 创建了第二个栈对象 s2 (根据同一张蓝图建了第二套房)// 对s1进行操作s1.Init(10); s1.Push(1); s1.Push(2); std::cout << s1.Top() << std::endl; // 输出s1的栈顶元素// 对s2进行操作s2.Init(5); s2.Push(100); std::cout << s2.Top() << std::endl; // 输出s2的栈顶元素// 关键点:s1和s2互不影响!std::cout << s1.Top() << std::endl; // 输出s1的栈顶元素,仍然是2。s2的操作没影响s1。return 0;
}
2.3 对象的大小
当用一个类实例化出一个对象时,就会有独立的数据空间。对象的数据空间中一定包含成员变量,但不包含成员函数;函数被编译后是一段指令,对象没办法存储。如果要存储,只能存储函数指针。但是函数指针没必要进行存储,类实例化成员函数时,即使成员函数不同,函数指针也是一样的,假设实例化100个对象,那么重复存储100次就会很浪费,所以C++中函数指针不需要存储,编译链接时就会找到函数的地址,而不是在运行时找。
C++中类实例化的对象也要遵循C语言中提到的内存对齐的规则。
内存对齐:
- 第⼀个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 对齐数=编译器默认的⼀个对齐数与该成员大小的较小值。
- VS中默认的对齐数为8。
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最⼤对齐数的整数倍处,结构体的整体大小 就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include<iostream>
using namespace std;
// 计算⼀下A / B / C实例化的对象是多⼤?class A
{
public:void Print(){cout << _ch << endl;}
private:char _ch;int _i;
};
class B
{
public:void Print(){//...}
};
class C
{};
int main()
{A a;B b;C c;cout << sizeof(a) << endl;cout << sizeof(b) << endl;cout << sizeof(c) << endl;return 0;
}
输出结果:
类B和C,没有成员变量和成员函数为空,大小为1个字节,是为了占位标识对象存在,来表示对象存在过。
3 this指针
#include<iostream>
using namespace std;
class Date
{
public:// void Init(Date* const this, int year, int month, int day)void Init(int year, int month, int day){// 编译报错:左操作数必须为左值// 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和d2Date d1;Date d2;// d1.Init(&d1, 2025, 8, 30);d1.Init(2025, 8, 30);d1.Print();d2.Init(2025, 8, 31);d2.Print();return 0;
}
如上述代码,在Date类中有两个成员函数,函数体中没有对象的区分,假如当d1调用Init函数时,添加参数后,是如何知道访问的是d1还是d2呢?这里就有一个隐含的this指针知识。
编译器编译后,实际上都会在成员函数的形参第一个位置,增加一个类的类型指针。这个指针就叫做this指针。比如Date类中的Init真实原型就是void Init(Date* const this,int year,int month,int day)
C++中规定不可以在实参和形参的位置上显示this指针,可以在函数体内部使用this指针。
4 C++和C语言实现Stack对比
C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数 据,这是C++封装的⼀种体现,这个是最重要的变化。本质是⼀种更严格规范的管 理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,后面会不断地说明。
C++中有⼀些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地 址,因为this指针隐含的传递了,方便了很多
C语言实现Stack:
#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++实现Stack:
#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;
};
本章完,如果这篇文章对你有所帮助,可以点点赞哦,你的支持就是我写下去的动力,后续会继续写类和对象的其他知识。