【C++】—— 类和对象(上)
文章目录
- 【C++】—— 类和对象(上)
- 前言
- 1. 类的定义
- 1.1 类定义格式
- 1.2 访问限定符
- 1.3 类域
- 2. 实例化
- 2.1 实例化概念
- 2.2 对象的大小
- 3. this指针
- 4. C++和C语言实现Stack对比
- 结语
前言
小伙伴们大家好呀,今天我们就开始学习C++的重点及难点——类和对象
这一章的内容非常的重要,让我们一起来揭开它神秘的面纱,好好看看吧
1. 类的定义
1.1 类定义格式
C++中类的定义与C语言中的结构体十分相似,像结构体一样,class是类的关键字,Date是类的名字,类体中内容称为类的成员:类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数
比方说,我们写日期类应该怎么写呢
class Date
{
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
一个日期类是不是这样写的
需要注意的是:
- 成员变量和函数可以在类位置可以任意顺序,一般是成员在下,函数在上
- 注意类定义结束时后面分号不能略
- 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前面或者后面加_或者m开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求
- C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类
- 类的名字就是类的类型。所以我们定义类直接用类名定义即可
- 定义在类面的成员函数默认为inline(内联)。但声明和定义分离,声明在类里就不是内联了
1.2 访问限定符
访问限定符有三种:
- private(私有)
- protected(保护)
- public(共有)
C++⼀种实现封装的方式,用类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
所以我们可以把日期类改成:
class Date
{
public:
void DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
通常情况下,我们不希望类中的成员变量被修改,所以使用私有private修饰,而对于成员函数我们想要在类外部进行对成员函数的调用,所以我们将成员函数进行公有修饰public
关于访问限定符我们要注意的是:
- public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private目前可以认为是⼀样的
- 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后面没有访问限定符,作用域就到
}
即类结束。一种访问可以出现多次。但一般同一作用域放在一起 - class定义成员没有被访问限定符修饰时默认为private,struct默认为public
- ⼀般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。因为一般我们不想别人修改我们数据,只需要给别人调用我的函数接口
1.3 类域
类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域
现在我们希望类看起来简洁一些,可以将成员函数的定义放到外面,在类中留下成员函数的声明
所以我们又可以把日期类改为:
class Date
{
public:
void DateInit(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
void Date::DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
类域影响的是编译的查找规则,倘若不写上 Date:: ,那么编译器会把成员函数当作是全局函数,那么在编译的时候,就找不到函数体中的 _year , _month , _day
,只有让编译器知道了这是一个类的成员函数,在编译的时候就会到类中去寻找 _year , _month , _day
这些变量
2. 实例化
2.1 实例化概念
类是对象进行⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间
就好比说类是一张房子的设计图,而对象是根据图纸制造出来的房子, 设计图规划了有多少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据
还是举一个例子
class Date
{
public:
void DateInit(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
void Date::DateInit(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
int main()
{
Date d1;
d1.DateInit(2024, 10, 3);
return 0;
}
现在我们在主函数中创建的 d1
,就是Date类进行实例化出来的对象,只有创建对象之后,我们类中的成员变量才有了空间
那么,一个类对象的大小应该怎么计算呢,接下来我们看看类对象的大小
2.2 对象的大小
一个类对象大小还得看它里面存储的是啥?
那存储的是啥,类里面 只存储了类成员变量,不存储成员函数
我们知道,对象中的成员变量都是独立的,每个对象的成员变量不一定相同,而每个对象的成员函数也有不同吗?,对象中如果想储存成员函数,只能储存函数的指针,那么对于一样的函数,其函数指针也是一样的,那么就没必要将每个对象的成员函数都进行储存,否则就太浪费空间了,所以对象的大小不包括成员函数的大小
另外,对于对象的大小计算,也要符合内存对齐规则:
- 第一个成员在与结构体偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 注意:对齐数=编译器默认的一个对齐数 与 该成员大小的较小值
- VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
来个题目试试
// 计算⼀下
A实例化的对象是多⼤?
class A
{
public:
void Print()
{
cout << _ch << endl;
}
private:
char _ch;
int _i;
};
偏移量:占8个字节空间
大家可以想一下为什么要内存对齐呢
现代计算机的CPU通常在特定的字节边界上访问数据(如4字节或8字节),如果数据未对齐,CPU可能需要进行多次内存访问才能读取完整的数据。这会减慢访问速度,因此通过对齐,可以优化内存访问的效率
理解了没,再来看看这个例子
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;
}
代码运行结果如下:
A是8没问题。可是B和C都没有成员变量,前面我们说类只存储成员变量
那他们没有为什么不是0呢
我们说给出的类这种没有成员变量的类,怎么证明我们创建的对象存在呢,所以计算出的大小为1,表示该对象存在
3. this指针
我们刚才讲到了,不同对象中的成员函数是没有区别的,那么在调用成员函数的时候,函数是怎么知道这是哪个对象调用的呢,那么这⾥就要看到C++给了⼀个隐含的this指针解决这里的问题
对于我们刚才日期类中的 DateInit
函数,表面上它有三个参数,可实际上还有一个隐含的参数,就是this指针
void DateInit(Date* const this, int year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
类的成员函数中访问成员变量,本质都是通过this指针访问的,如函数中给 _year 赋值, this->_year = year
,可见,this指针是存放对象地址的指针
同时, C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针
来个题体会一下
this指针存在内存哪个区域的 ()
A. 栈 B.堆 C.静态区 D.常量区 E.对象里面
首先排出E选项。为什么?前面我们说了类对象里面只存储成员变量
其次又因为this指针是形参,形参是存储在栈帧里面,所以选A比较合理
但是VS下因为this指针会频繁使用,所以把this指针放在寄存器里面,这可以认为是VS做的优化
4. C++和C语言实现Stack对比
面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解⼀下封装
通过下面两份代码对比,我们发现C++实现Stack形态上还是发生了挺多的变化,底层和逻辑上没啥变化
C++实现栈:
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;
}
C语言实现栈:
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];
}
C++中数据和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,这个是最重要的变化。这里的封装的本质是⼀种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习
C++中有⼀些相对方便的语法,比如Init给的缺省参数会方便很多,成员函数每次不需要传对象地址,因为this指针隐含的传递了,方便了很多,使用类型不再需要typedef用类名就很方便
结语
这就是类和对象的初步了解,感觉是不是蛮简单的,类和对象这里内容比较多,大家就好好理解
好了,感谢你能看到这里,溜了溜了,我们下期再见吧