C++类和对象1
一、类的定义
1.类定义格式
class 为定义类的关键字,Stack 为类的名字, { } 中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员,类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。
为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加 “ _ ” 或者 “ m ” 开头,注意C++中这个并不是强制的,只是一些惯例。
C++中 struct 也可以定义类,C++兼容C中 struct 的用法,同时 struct 升级成了类,明显的变化是 struct 中可以定义函数,一般情况下我们还是推荐用 class 定义类。
定义在类里面的成员函数默认加上 inline 。
#include <iostream> class Stack { public: // 成员函数 void init(int n = 4) { _arr = nullptr; _capacity = _size = 0; checkCapacity(); } int top() { return _arr[_size - 1]; } void pop() { --_size; } void push(int x) { checkCapacity(); _arr[_size++] = x; } void destroy() { free(_arr); _arr = nullptr; _capacity = _size = 0; } void checkCapacity() { if (_capacity <= _size) { int newCapacity = _capacity == 0 ? 4 : _capacity * 2; int* temp = (int*)realloc(_arr, sizeof(int) * newCapacity); if (temp == nullptr) { perror("realloc faild"); return; } _arr = temp; _capacity = newCapacity; } } private: // 成员变量 int* _arr; int _capacity; int _size; }; int main() { Stack st; st.init(); st.push(1); st.push(2); st.push(3); st.destroy(); return 0; }
2.访问限定符
C++一种实现封装的方式,用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
public 修饰的成员在类外可以直接被访问,protected 和 private 修饰的成员在类外不能直接被访问,protected 和 private 在这方面的作用是一样的。(在继承中才可体现他们的区别)
访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 “ } ” 即类结束。
class 定义成员没有被访问限定符修饰时默认为 private,struct 默认为 public。
一般成员变量都会被限制为 private / protected,需要给别人使用的成员函数会修饰为 public。
class Student { // 访问限定符 public: protected: private: };
3. 类域
- 类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 “ :: ” 作用域操作符指明成员属于哪个类域。
- 类域影响的是编译的查找规则,下面程序中 init 如果不指定类域 Stack,那么编译器就把 init 当成全局函数,那么编译时,找不到 _arr 等成员的声明 / 定义在哪里,就会报错。指定类域 Stack,就是知道 init 是成员函数,当前域找不到的 _arr 等成员,就会到类域中去查找。
class Stack { public: void init(int x); private: int* _arr; int _capacity; int _size; }; // 声明和定义分离 void Stack::init(int x) { //... }
二、实例化
1.实例化概念
- 用类类型在物理内存中创建对象的过程,称为类实例化出对象。
- 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象才能分配物理内存存储数据。
2.对象大小
类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量。但成员函数不一样,首先函数被编译后是一段指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。并且对象中也没有存储其指针的必要,Date 实例化 d1 和 d2 两个对象,d1 和 d2 都有各自独立的成员变量 _year / _month / _day 存储各自的数据,但是 d1 和 d2 的成员函数 init / print 指针却是一样的,存储在对象中就浪费了。如果用 Date 实例化 100 个对象,那么成员函数指针就重复存储 100 次,太浪费了。
#include <iostream> using namespace std; class Date { public: void init(int year = 2000, int month = 1, int day = 1) { _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, 8, 2); d1.print(); d2.init(2014, 8, 2); d2.print(); return 0; }
上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。
内存对齐规则:
- 第一个成员在与结构体偏移量为 0 的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。
- VS中默认的对齐数为 8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
博客 (学习总结6)C语言结构体的内存对齐和位段实现 对内存对齐有详细的解释,这里不赘述。
没有成员变量的类为表示对象存在,给定 1 字节,纯粹为了占位标识对象存在。
#include <iostream> using namespace std; class Student { ; }; int main() { Student a; // 大小都为 1 字节 cout << sizeof(a) << endl; cout << sizeof(Student) << endl; return 0; }
三、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 指针。
this指针 存放在寄存器中。
class Date { public: //void init(Date* const this, int year = 2000, ...) void init(int year = 2000, int month = 1, int day = 1) { // 不能修改 //this = nullptr; //_year = year; //_month = month; //_day = day; this->_year = year; this->_month = month; this->_day = day; } void print() { cout << _year << "/" << _month << "/" << _day << endl;; } private: int _year; int _month; int _day; };
四、C++和C语言实现Stack对比
面向对象三大特性:封装、继承、多态,下面的对比可以初步体现封装。
通过下面两份代码对比,我们发现C++实现 Stack 形态上还是发生了挺多的变化,底层和逻辑上没啥变化。
C语言版本 Stack,详细可参考博客 栈的讲解与实现
#include <stdio.h> #include <stdlib.h> #include <assert.h> #include <stdbool.h> #define INIT_CAPACITY 4 #define EXPANSION_MULTIPLE 2 typedef int STDataType; typedef struct Stack { STDataType* arr; int size; int capacity; } Stack, * pStack; bool StackEmpty(pStack pst) { assert(pst); return pst->size == 0; } void StackInit(pStack pst) { assert(pst); pst->arr = NULL; pst->size = 0; pst->capacity = 0; } void StackDestroy(pStack pst) { assert(pst); free(pst->arr); pst->size = 0; pst->capacity = 0; } void StackPush(pStack pst, STDataType x) { assert(pst); if (pst->size == pst->capacity) { int newCapacity = pst->capacity == 0 ? INIT_CAPACITY : pst->capacity * EXPANSION_MULTIPLE; STDataType* temp = (STDataType*)realloc(pst->arr, newCapacity * sizeof(STDataType)); if (temp == NULL) { perror("realloc failed"); return; } pst->arr = temp; pst->capacity = newCapacity; } pst->arr[pst->size++] = x; } void StackPop(pStack pst) { assert(pst); assert(!StackEmpty(pst)); --pst->size; } STDataType StackTop(pStack pst) { assert(pst); assert(!StackEmpty(pst)); return pst->arr[pst->size - 1]; } int StackSize(pStack pst) { assert(pst); return pst->size; }
C++版本 Stack:
#include <iostream> #include <assert.h> using namespace std; const int INIT_CAPACITY = 4; const int EXPANSION_MULTIPLE = 2; typedef int STDataType; class Stack { public: bool empty(); void init(); void destroy(); void push(STDataType x); void pop(); STDataType top(); int size(); private: STDataType* _arr; int _size; int _capacity; }; bool Stack::empty() { return _size == 0; } void Stack::init() { _arr = nullptr; _size = _capacity = 0; } void Stack::destroy() { free(_arr); _arr = nullptr; _size = _capacity = 0; } void Stack::push(STDataType x) { if (_size == _capacity) { int newCapacity = _capacity == 0 ? INIT_CAPACITY : _capacity * EXPANSION_MULTIPLE; STDataType* temp = (STDataType*)realloc(_arr, newCapacity * sizeof(STDataType)); if (temp == nullptr) { perror("realloc failed"); return; } _arr = temp; _capacity = newCapacity; } _arr[_size++] = x; } void Stack::pop() { assert(!empty()); --_size; } STDataType Stack::top() { assert(!empty()); return _arr[_size - 1]; } int Stack::size() { return _size; } int main() { Stack a; a.init(); a.push(1); a.push(2); a.push(3); a.push(4); while (!a.empty()) { cout << a.top() << endl; a.pop(); } a.destroy(); return 0; }
- C++中数据都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,这里就不展开讲了。
- C++中有一些相对方便的语法,比如缺省参数,成员函数每次不需要传对象地址,因为 this 指针隐含的传递了,方便了很多,使用类型不再需要 typedef 类名就很方便。