文章目录
一、class – 类
C++ 把 C 语言中的 结构体(struct) 升级成了 类(class),将 数据 和 方法 都封装进类里面,其中的成员会在类域中被全局搜索,没有前后之分。
1.1 类的各个部分介绍
- class:类的关键字
- ClassName:类的名字,建议大驼峰书写
- { }:括号中的是类的主体,放类的成员,注意后面 分号不能省略
- 类的成员:分为两部分 –>
- 类中的 变量 称作 类的属性 或 成员变量
- 类中的 函数 称为 类的方法 或 成员函数
class className
{
// 类体:成员函数 + 成员变量
};
1.2 类定义的两种方式:
-
声明和定义都在类体中,在类体中定义函数,会有编译器将其处理成内联函数的风险,一般不推荐这样写。
-
声明和定义 分开编写 在 .h 和 .cpp 文件中,推荐这种写法(案例所示)。每个类都是一个域,写函数名时需注意加上作用域操作符 –>
类名::函数名
/*********************** 声明 *********************/
/* class.h */
class Date
{
public:
void Init(int year);
private:
int _year;
};
/*********************** 定义 *********************/
/* class.cpp */
#include"class.h"
void Date::Init(int year)
{
year = _year;
}
1.3 访问限定符
C++ 用类将对象的属性和方法 封装 在一块,并通过以下三种 访问限定符 对成员进行修饰从而控制访问权限,选择性的将接口提供给给外部用户使用:
public
(公有):其修饰的对象在类外可直接被访问protected
(保护):不能直接被访问private
(私有):不能直接被访问
【使用规则】:
- 访问权限作用域,从 该访问限定符出现的位置 开始直到 下一个访问限定符出现 时为止,如果后面没有访问限定符,作用域就到 } 即类结束。
- 如果不设置,class 的默认访问权限为 private,struct 为 public(因为 C++ 需要兼容 C)。
二、对象(Object)
2.1 对象和类的关系 – 对象是类的实例化
用类创建对象的过程,就是类的实例化。也就是说,对象 是 类实例化 的结果
同一张建筑图纸,可以造出多栋房子。类好比图纸,实例化的对象好比建造出来的房子。
【类可以实例化出 多个对象,实例化出的对象,才占用物理空间。】
2.2 对象的大小
既然对象是被类实例化出来,真实存储的,那么他的大小该如何计算呢?
实际上:
每个对象的 成员变量是不尽相同的,需要独立存储;
而每个对象 调用的成员函数是一样的,只需放到共享公共区域(代码段);
【总结:类的对象中,只存了成员变量,没有存成员函数。类的大小,实际上就是该类中 “成员变量” 之和,注意 内存对齐规则】
/***************************** 类的实例化、对象的大小 ****************************/
class Date ---------------------------------------> 类
{
public:
void Init(int year)
{
year = _year;
}
private:
int _year;
};
int main()
{
Date d1; --------------------------> d1 就是 Date 这个类,实例化出的对象
Date d2; ---------------------------> d2 也是 Date 类,实例化出的对象
d1.Init(2023);
cout << sizeof(d1) << endl; // 初始化
cout << sizeof(d2) << endl; // 未初始化
return 0;
}
------
输出结果:
4
4
当类中 只有成员方法 或干脆是个 空类 时,实例化这个类,编译器会给 1byte 的空间,这 1byte 不存放有效数据,只用来占位,标识对象已经被实例化定义出来了。
// 只有成员方法
class A1{
public:
void f1(){}
};
// 空类
class A2{
};
// 调用
int main()
{
A1 a1;
A2 a2;
cout << sizeof(a1) << endl;
cout << sizeof(a1) << endl;
return 0;
}
------
输出结果:
1
1
2.3 this 指针
可以借由上述类的实例化案例来思考,d1 和 d2 若都调用 Init() 函数,
传入参数相同的情况下,Init() 函数怎么确定数据该存入 对象d1 还是 对象d2 处呢?
this 指针是一个隐形的形参,只在 类的成员函数 被调用时,编译器才自动处理、转换出来的一条指令
用于 分辨调用对象,即,this 指针是对象的地址。
this 指针是调用函数时才产生的,是一个形参,所以存在栈中。(vs 下是通过 ecx 寄存器存的)
/************** 编译器处理后的样子 ****************/
class Date
{
// ...
void Init(Date* this,int year) -----> 函数形参中出现 this 指针
{
this->_year = year;
}
// ...
};
int main()
{
Date d1;
Date d2;
d1.Init(&d1,2023); -------------> 调用函数时传入了对象的地址
d2.Init(&d2,2017);
return 0;
}
这样的 this 指针 是编译器直接转化成的指令,不需要我们操作。
可以把它想做隐形却存在的参数,实际编写代码时不论函数形参还是实参部分都不能画蛇添足去加写,但是可以在函数里面使用,比如 cout 这个 this 指针会打印出来调用该函数的对象的地址。
举例数据结构,从类的封装,浅浅分析 C 和 C++
都是语言,在底层(空间大小、调用函数...)上没有区别,只是理念不同
【C】:
- 数据和方法是分离的
- 数据访问的控制是自由的,不受限制的(例1),太随意了
【C++】:
- 数据和方法都封装到类里面
- 控制了访问方式,愿意让用户访问的 public, 不愿意的就 private(例2)
// 例1:取栈顶元素,接口的设置是为了安全访问,因为初始化 top=0或者1 是底层结构,后面使用的人如果直接访问很容易出问题
// 推荐使用 StackTop() 接口,而不推荐直接使用,因为 1.不安全 2.需要了解底层结构
// but!!推荐不是强制,还是会有人“闯红灯”
int mian()
{
ST st;
StackInit(&st);
StackPush(&st, 1);
//..
int top = StackTop(&st);
int Top = st.a[st.top - 1]; // 首先不说需要程序员知道,是否需要-1,这个行为还很可能导致越界访问
return 0;
}
// 例2:同 取栈顶元素
int main()
{
Stack st;
st.Init();
st.Push(1);
//...
int top = st.Top(); // the only way... 封装控制着,强制消解了在这步出错的可能性
}
C++ 设置 封装 的意义:更好的规范、分类、管理!!
面试题两道
1、this 指针 存在哪里?
(略)
2、关于 空指针:分析其运行情况( 编译报错? 运行崩溃? 正常运行?)
分析:func() 和 Init() 不储存在对象中,
ptr-> 意味着指针调用成员函数,编译器用 call 在公共区域(代码段)找,即使是空指针,这部分的调用不会有问题
调用上了函数,而 fun() 只是简单的打印,Init() 里面 this 是空指针,还想对数据解引用,越界访问了,所以导致运行崩溃
所以哦,不是所有 空指针+解引用符号 都有问题,需要具体分析
class Date
{
// ...
void func()
{
cout << "func()" << endl;
}
void Init(int year)
{
_year = year;
}
// ...
};
/************************** 题目 **************************/
Date* ptr = nullptr;
4. ptr->func(); // 正常运行
5. ptr->Init(2022); // 运行崩溃
6. (*ptr).func(); // 正常运行
接下篇【Ⅱ】默认成员函数篇:构造+析构、拷贝构造+赋值重载、取地址重载
【C++入门】类和对象Ⅱ:默认成员函数、构造函数、析构函数、拷贝构造函数、赋值运算符重载函数、取地址及const取地址操作符重载(含例题:时间计算器)