1.类的定义
1.1 类定义格式
- class为定义类的关键字,Stack为类的名字,{ }中的内容是类的主题为了,注意类定义结束时后面的分号不能省略。类体中的内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
- 为了区分成员变量,一般习惯上成员变量会加上一个特殊标识,如成员变量前面或者后面加上_或者是m开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。
- C++中struct也可以定义类,C++兼容C中的struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,一般情况下我们还是推荐用class定义类。
- 定义在类里面的成员函数默认为inline。
//定义一个类
class Date
{void Init(int year, int month, int day){_year = year;_month = month;_day = day;}//为了好区分到底是成员变量还是参数,一般在成员变量前面加上_或是mint _year;int _month;int _day;};//C++中兼容C语言中的struct的用法
typedef struct ListNodeC
{struct ListNodeC* next;int val;
}ListNode;//在C++中struct升级为类了
//不再需要typedef,ListNodeCPP就可以代表类型struct ListNodeCPP
{void Init(int x){next = nullptr;val = x;}ListNodeCPP* next;int val;
};int main()
{ListNode node1;struct ListNodeC node2;ListNodeCPP node3;return 0;
}//class类如果没有访问限定符的修饰,默认是private
//struct类如果没有访问限定符的修饰,默认是publicclass Stack
{//默认在类里面的成员函数都是内联,但是展不展开是编译器的事情
public:void Init(int n=4){arr = (int*)malloc(sizeof(int) * n);if (arr == nullptr){perror("realloc fail!\n");return;}capacity = n;top = 0;}void Push(int x){// ...扩容arr[top++] = x;}int Top(){assert(top > 0);return arr[top - 1];}void Destroy(){free(arr);arr = nullptr;top = capacity = 0;}private:int* arr;size_t top;size_t capacity;
};int main()
{Date d1;//error C2248 : “Date::Init” : 无法访问 private 成员(在“Date”类中声明)d1.Init(2024, 11, 11);//如果想要Init被访问在Date类加上public,就可以被访问了//或者是将class类改为struct类Stack st1;st1.Init(10);st1.Push(1);st1.Push(1);st1.Push(1);//int top = st1->arr[st1->top - 1]; C中实现top方法会更加自由,不能进行更好的管理int top=st1.Top();//C++的模式下会更加规范,会更好的进行管理,因为私有成员不能访问,只能访问公有函数st1.Destroy();return 0;
}
1.2 访问限定符
- C++一种实现防撞的方式,用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
- publi修饰的成员在类外可以直接被访问;protected 和 private 修饰的成员在类外不能直接被访问,protected 和 private 是一样的,在之后的继承章节才能体现出它们的区别。
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就到 } 即类结束。
- class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
- 一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会为public。
1.3 类域
- 类定义一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
- 类域影响的是编译的查找规则,下面程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。
//Stack.h#include<iostream>
using namespace std;
class Stack
{
public://成员函数 唯一不同的:只声明不定义void Init(int n = 4);//缺省参数:(int n = 4)不能在定义和声明的地方同时给,只能在声明的地方给
private:int* arr; //声明——不开空间size_t top;size_t capacity;
};
//Stack.cpp#define _CRT_SECURE_NO_WARNINGS 1#include"Stack.h"//声明和定义分离,需要指定类域
//加上 Stack:: 类域,就不会报错,arr会到Stack这个域里面去找,否则arr不知道去哪里找
//不加上Stack:: 类域,“arr”: 未声明的标识符//error C2065 : “capacity”: 未声明的标识符//error C2065 : “top”: 未声明的标识符
void Stack::Init(int n)
{//这里的Init还是符合只能在类里面使用,不是在类外面,只是类的声明和定义分开了而已//还是符合私有的在类里面使用不能在类外面使用arr = (int*)malloc(sizeof(int) * n);if (arr == nullptr){perror("realloc fail!\n");return;}capacity = n;top = 0;
}
//Test.cppint main()
{Stack st;st.Init();return 0;
}
类域的作用:
不同的域中可以定义同样的函数,隔离出了类和类之间的命名冲突
2. 实例化
2.1 实例化概念
- 用类型在物理内存中创建对象的过程,称为类实例化出的对象。
- 类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。例如:
- 类实例化处对象就像是现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了多少个房间,房间大小功能,但是没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像是设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。
//Stack.h#include<iostream>
#include<assert.h>
using namespace std;
class Stack
{
public://成员函数 唯一不同的:只声明不定义void Init(int n = 4);//缺省参数:(int n = 4)不能在定义和声明的地方同时给,只能在声明的地方给
//private:int* arr; //声明——不开空间size_t top;size_t capacity;
};
//类实例化对象——对象是实实在在需要内存的,在内存上面存储数据的#include"Stack.h"
int main()
{//类实例化对象// 对象定义,也就是成员变量的定义,因为成员变量是对象的一部分//定义——开空间,定义不等于初始化Stack st1; //这就是类实例化对象Stack st2; //这就是类实例化对象Stack st3; //这就是类实例化对象//st1、st2、st3才有空间//st1.top = 1; (将成员变量改为public,否则不能访问)虽然这样是正确的,因为定义之后有空间了, 但是这样的写法不规范,因为top是私有的,//要想改变的话,通过公有的函数来进行改变,这就使C++更加规范//Stack::top = 1;//top没有空间,不能这样写//对象不一定有声明,但是类是必须要有声明的,如果类没有声明的话,怎么能用类去实例化对象呢return 0;
}
2.2 对象大小
对象的大小 —— 只计算成员变量,不计算成员函数,遵循内存对齐原则
内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- VS中默认的对齐数是8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是在所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
#include"Stack.h"
int main()
{//定义——开空间,定义不等于初始化Stack st1; //类实例化对象Stack st2; Stack st3; cout << sizeof(st1) << endl; //12 x86cout << sizeof(Stack) << endl; //12 x86st1.top = 1; //(将成员变量改为public,否则不能访问)st1.Init(); // call 函数地址(不需要存在对象里面)st2.top = 2; //(将成员变量改为public,否则不能访问)st2.Init();return 0;
}
运行结果:
为什么运行出的结果都是 12 ???
调试 ---> 转到反汇编
通过调试,转到反汇编,可以看见st1.Init(); 和 st2.Init();调用的都是相同的地CA104Bh)
而 st1 和 st2 各自开空间存储自己的成员变量,指向的不是同一块地址
成员函数的地址不在对象里面,那成员函数的地址又是声明时候确定的呢??
函数的地址是在编译的时候确定的
所以:
对象的大小 —— 只计算成员变量,不计算成员函数,遵循内存对齐原则
// 计算一下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; //8cout << sizeof(b) << endl; //1cout << sizeof(c) << endl; //1return 0;
}
运行结果:
分析:
- 类A遵循内存对齐原则,所以大小为:8
- B和C一样:成员函数不占空间,为什么是 1 ???
系统设的机制,B和C都是空类,虽然B有成员函数,成员函数不占空间,1 是为了占位,表示对象存在过,如果1个字节都不给的话,怎么能表示这个对象定义出来了呢?如果一个字节都不开,地址用怎样给呢?这里开一个字节不存储有效数据,纯粹是为了占位,表示这个对象还在,那为什么不是 2、3呢?肯定是越少越好嘛,节省空间
3. this 指针
- Date类的d1和d2涉及到的Init和Print函数都是相同的空间,但是打印出来的值为什么是不同的呢??难道是因为 Init 传了参数,Print 没有传参数 ??? —— 答案不是这样的,引出 this 指针
- 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做 this 指针。实际上:void Init(const Date* this,int year, int month, int day)
- 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值,this- >_year = year;
- C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是不能在实参或形参的位置显示的加,但是可以在函数里面使用 why???? ---->之后会提到
class Date
{
public://实际上:void Init(const Date* this,int year, int month, int day)void Init(int year, int month, int day){this->_year = year; //_year访问的是344行的_year吗?不是,因为没空间,编译语法设计的方面//用一个变量和函数要找到他的出处,出处可以是定义,也可以是声明,意味着这个变量和和函数是你自己定义的,在编译的时候找到他的声明就够了this->_month = month; //但是在打印和初始化设计的是实实际际的空间this->_day = day;}//实际上:void Print(const Date* this)void Print(){cout << _year << "/" << _month << "/" << _day << endl;}//为了好区分到底是成员变量还是参数,一般在成员变量前面加上_或是m
private:int _year; //344行int _month;int _day;};int main()
{Date d1;Date d2;d1.Init(2024, 7,3);d2.Init(2024, 4,27);// d1.Print(&d1); d1.Print(); // 2024 / 7 / 3// d2.Print(&d2); d2.Print(); // 2024 / 4 / 27}
分析:
这里的d1和d2涉及到的Init和Print函数都是相同的空间,但是打印出来的值为什么是不同的呢??
难道是 Init 传了参数,Print 没有传参数 ??? —— 引出 this 指针
隐含的this 指针 —— 隐含的指的是:编译器会在成员函数的实参和形参的位置加一个this指针的参数
void Init(int year, int month, int day)看见的是3个参数,实际是4个参数
void Print()看见的是0个参数,实际上是1个参数,实际上是编译器加的
void Init(const Date* this,int year, int month, int day)
void Print(const Date* this)
在变量访问的地方会在成员变量前面自动加上 this->
所以在调用同一个函数,访问到了不同的变量
在调用d1.Print(); 和 d2.Print();的时候分别将d1和d2的地址传过去了
所以用this指针在调用同一个函数,访问到了不同的变量,但是不能在实参或形参的位置显示的加,但是可以在函数里面使用 why???? ---->之后会提到
相应题目:
1.下面程序编译运行结果是()
A.编译报错 B.运行崩溃 C.正常运行
class A
{
public:void Print(){cout << "A::Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print(); return 0;
}
运行结果:
可以看见代码并没有问题,正常运行,所以选C
分析:
p->Print();
从汇编的角度:call Print(地址),地址是在编译的时候确定的
虽然这里有一个 -> 但是却没有解引用,不是说看见箭头就是解引用,要去看它实际转换成的动作是什么,实际的动作是调用函数,在不在对象里面??—不在
p的作用是:
1.编译器调用成员函数,编译器要知道Print()成员函数从哪里调的,用对象的指针去调用就知道他是他(类)的成员函数,编译的时候符合语法找Print的出处,就到类里面去找
2.调用成员函数要传递this指针,相当于将p传给了this,但是this指针是一个空指针,是不会报错的
所以从始至终都有空指针,但是并没有进行解引用,所以不会报错
2.下面程序编译运行结果是()
A.编译报错 B.运行崩溃 C.正常运行
class A
{
public:void Print(){cout << "A::Print()" << endl;cout << _a << endl; // cout << this->_a << endl; 空指针的访问 }
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
运行结果:
代码和上一道题几乎一样,但是可以看见运行有问题,只是因为对空指针进行了解引用操作
分析:
this 指针是一个空指针,用代码来验证一下:
class A
{
public:void Print(){cout << this << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
运行结果:
上面的运行结果验证了 this 确实是空指针,所以下面的代码会报错,因为不能对空指针进行进引用操作
cout << _a << endl;
cout << this->_a << endl; 空指针的访问
考点:
1.对象里面没有存储成员函数的指针
2.隐含的传this指针,谁传过去?对象——就传对象的地址,指针——直接就是指针传过去,成员变量前面要加this:this->_a3.所以这道题选A
上面的两个例子初不初始化都无所谓,大不了就是随机值,跟初始化没有关系
3.this指针存在内存哪个区域中()
A.栈 B.堆 C.静态区 D.常量区 E.对象里面
分析:
所以这道题 选 A.栈
拓展:
4. C实现Stack核心代码 和 C++实现Satck核心代码对比:
面向对象显著的三大特性:封装、继承、多态(当然还有其它的特性),下面是对封装的初步介绍:
- C+中的数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意的通过对象直接修改数据,这就是C++封装的一种体现,这个是最重要的变化。这里的封装的本质是一种更严格规范的管理,避免出现访问修改的问题。
- C++中会有一些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,方便了许多。