爆学C++之类和对象(上)

凯子坚持 c 2024-08-20 08:05:02 阅读 74

1.类的定义

类定义格式

• class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的⽅法或者成员函数。

• 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加_ 或者 m开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求。

• C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。

• 定义在类⾯的成员函数默认为inline。

类和结构体最大的不同是类里面能定义函数

那么我们使用类型名+点+函数名那么我们就能调用这个函数了

如果是指针的话,那么我们用箭头就行了

类的定义--拿栈来举例:

<code>class Stack

{

void Init(int n = 4)//给出一个缺省参数,缺省值默认给的是4

{

array = (int*)malloc(sizeof(int) * n);

if (nullptr == array)

{

perror("malloc fail!");

return;

}

capacity = n;

top = 0;

}

void Push(int x)

{

//扩容

array[top++] = x;

}

int Top()

{

assert(top > 0);

return array[top - 1];

}

void Destroy()

{

free(array);

array = nullptr;

top = capacity = 0;

}

//栈

int* array;

size_t capacity;

size_t top;

};

//类名就是类型

int main()

{

Stack st1;//我们直接用这个类名当成类型来定义对象

//我们初始化的话直接就是类型加点再加函数名,那么我么家就调用了这个函数

st1.Init();

st1.Push(1);

st1.Push(2);

st1.Push(3);

st1.Push(4);

cout << st1.Top() << endl;

st1.Destory();

return 0;

}

//那么我们定义完类的结构,但是我们这里为什么显示的是错的呢?

//那么多报错

//就说明我们这个类还不是完全体,那么我们就需要进行后面的学习了

访问限定符

• C++⼀种实现封装的⽅式,⽤类将对象的属性与⽅法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接⼝提供给外部的⽤⼾使⽤。

• public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。

• 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到 }即类结束。

• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

• ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。

三种访问限定符

struct兼容C语言的用法,又有C++的用法

那么struct用C++的用法的时候和class有什么区别呢?

class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

就是我们在使用class来定义类的话,我们没有使用访问限定符,那么这个类中默认的就是私有的

这种情况的话struct是公有的

这个就是class和struct的唯一的区别点

class默认是私有

struct默认是公有

那么学习了访问限定符之后,那么我们的类就算定义完成了

在类定义完之后我们还挖掘了struct在C/C++两个方向的不同以及struct和class的区别

类的完整定义以及struct来定义类:

<code>class Stack

{

public:

//成员函数

void Init(int n = 4)//给出一个缺省参数,缺省值默认给的是4

{

array = (int*)malloc(sizeof(int) * n);

if (nullptr == array)

{

perror("malloc fail!");

return;

}

capacity = n;

top = 0;

}

void Push(int x)

{

//扩容

array[top++] = x;

}

int Top()

{

assert(top > 0);

return array[top - 1];

}

void Destroy()

{

free(array);

array = nullptr;

top = capacity = 0;

}

private:

//成员变量

//栈

int* array;

size_t capacity;

size_t top;

};

struct Person//利用结构体也定义了一个类 那么类名就是类型

{

public://公有化

void Init(const char* name, int age, int tel)

{

strcpy(_name, name);//将name复制过去

_age = age;

_tel = tel;

}

void Print()

{

cout << "姓名:" << _name << endl;

cout << "年龄:" << _age << endl;

cout << "电话:" << _tel << endl;

}

private://私有化

char _name[10];

int _age;

int _tel;

};

//队列

typedef struct QueueNode

{

struct QueueNode* next;

int val;

}QNode;

typedef struct Queue

{

QNode* head;

QNode* tail;

int size;

}QU;

void QueueInit(QU* q)

{

q->head = nullptr;

q->tail = nullptr;

q->size = 0;

}

//类名就是类型

int main()

{

Stack st1;//我们直接用这个类名当成类型来定义对象

//我们初始化的话直接就是类型加点再加函数名,那么我么家就调用了这个函数

st1.Init();

st1.Push(1);

st1.Push(2);

st1.Push(3);

st1.Push(4);

cout << st1.Top() << endl;

st1.Destroy();

Person p1;

p1.Init("张三",18,132);

p1.Print();

QU qu;

QueueInit(&qu);

return 0;

}

//我们在这里定义public 公有的

//直到遇到下一个访问限定符,如果没有下一个访问限定符的话

//那么这个访问限定符到};这个中间的空间都是外界能够进行访问的

//我们在函数后面加上private,那么这些函数就是公有的

//那么private到};中间的就是私有的

//一般来说的话成员函数一般是公有的

//成员的变量一般是私有的

//那么到这里我们就真正定义出了一个类

//类中有成员变量也有成员函数

//我们现在是能对类里面的的函数进行访问的,但是不能对类里面的变量成员进行访问,因为这个是我们的私有的

//我们能用struct来定义类,但是我们还是推荐用class

//那么我们通过这种队列的案例我们发现在C++中

//我们的struct既能像c语言一样用struct来定义结构体,也能像C++一样用struct来定义类

//在类里面定义成员函数默认是内联函数 前面没有加inline那么默认也加了inline

//struct兼容C语言的用法,又有C++的用法

//那么struct用C++的用法的时候和class有什么区别呢?

类域

• 类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作⽤域操作符指明成员属于哪个类域。

• 类域影响的是编译的查找规则,下⾯程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪⾥,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。

我们在之前学到,C++中有四个域:局部域、全局域、命名空间域和类域

域的作用是可以作为名字的隔离

同一个域不能定义同名

不同的域能够定义同名

命名空间域和类域不会影响生命周期的,局部域和全局域是会影响生命周期的

函数声明定义分离的时候我们要指定类域:

下面我们就看看是如何使用的

以栈的初始化为例

test.cpp

#include"Stack.h"

int main()

{

Stack st;// 类中的栈 定义一个栈

st.Init();//栈的初始化

return 0;

}

//这个就是标准的类的声明和定义分离

//我们在Stack.cpp文件中指定我们要查找的函数,前面带上指定的类域

//那么编译器就会到指定的类域进行搜索

//函数声明定义分离的时候我们要指定类域

Stack.cpp

#define _CRT_SECURE_NO_WARNINGS 1

#include"Stack.h"

//我们在类外面定义函数

void Stack::Init(int n )

//为了让编译器找到我们类中的函数

//我们需要在前面加上Stack::

//在前面指定类域

{

_a = (int*)malloc(sizeof(int) * n);

if (nullptr == _a)

{

perror("malloc fail!");

return;

}

_capacity = n;

top = 0;

}

Stack.h

#pragma once

#include<iostream>

#include<stdlib.h>

#include<assert.h>

using namespace std;

//类的基本结构

class Stack

{

public:

void Init(int n = 4);//缺省参数在声明和定义都在的时候只能给声明,不能给定义

private:

int* _a;

int top;

int _capacity;

};

2.实例化

实例化的概念

• ⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。

• 类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只 是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。

• ⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个⽐ ⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多 少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房 ⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。

#include<iostream>

using namespace std;

class Data

{

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;

};

//定义:系统在这里开了空间

//int year;

int main()

{

Data d1;//用类型定义出了个对象,这个对象里面有年月日这些数据

//那么这种操作就是类实例化出对象

Data d2;

//1个类可以实例化出n个对象

//创建出对象之后我们就能调用类中对应的方法

d1.Init(2024, 6, 5);

d2.Init(2024, 9, 8);

d1.Print();

d2.Print();

return 0;

}

//类里面是不能存真实数据的,我们需要先用这个类实例化出一个对象

//然后这个对象里面就能进行存数据的操作

//这个类就是一个图纸

//我们根据这个图纸进行打造对象

//然后再对对象进行操作

我们先将类的结构定义好

然后用这个类当做一个图纸来定义出了个对象出来

然后对这个对象进行数据的一些操作

但是我们的类是不能进行数据的操作的

对象大小

分析⼀下类对象中哪些成员呢?类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含 成员变量,那么成员函数是否包含呢?⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令 存储在⼀个单独的区域(代码段),那么对象中⾮要存储的话,只能是成员函数的指针。再分析⼀下,对 象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量 year/month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象 中就浪费了。如果⽤Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这⾥需 要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指 令[call 地址],其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在 运⾏时找,就需要存储函数地址,这个我们以后会讲解。

<code>class Data

{

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;

};

//定义:系统在这里开了空间

//int year;

int main()

{

Data d1;//用类型定义出了个对象,这个对象里面有年月日这些数据

//那么这种操作就是类实例化出对象

Data d2;

//1个类可以实例化出n个对象

//

//创建出对象之后我们就能调用类中对应的方法

d1.Init(2024, 6, 5);

d2.Init(2024, 9, 8);

d1.Print();

d2.Print();

cout << sizeof(d1) << endl;//12

cout << sizeof(d2) << endl;//12

//说明类的实例化的对象里面只存了成员变量‘

//这里的话每个变量大小都是4个字节

//那么总大小就是12个字节

/*

这个的话d1和d2的数据是不一样的

d1和d2调用的Print是一样的

为什么我们的对象的大小仅仅是这些变量

没有带上这些函数呢?

因为我们调用的函数都是一个函数

仅仅是对象中的变量的数据不同

如果我们将这个函数的大小算进去的话

就会显得很冗余

函数就存在公共的区域

总结:每个类可以有很多个对象

对象中要存成员变量

每个对象的成员变量的数据都不一样

但是每个对象要使用的函数都是一样的、

那么如果我们将这个函数的指针存在对象中那么就是冗余现象了

这些函数都是公共的

所以我们对象的大小仅仅包含这些成员变量

那么我们在计算类的对象的大小的时候我们只考虑成员变量,不考虑成员函数

*/

return 0;

}

//类里面是不能存真实数据的,我们需要先用这个类实例化出一个对象

//然后这个对象里面就能进行存数据的操作

//这个类就是一个图纸

//我们根据这个图纸进行打造对象

//然后再对对象进行操作

我们类定义出的对象的大小不包括成员函数

只包括成员变量

那么计算成员变量的大小就和C语言中求结构体的大小的规则是一样的

需要遵循内存对齐规则

• 第⼀个成员在与结构体偏移量为0的地址处。

• 其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。

• 注意:对⻬数=编译器默认的⼀个对⻬数与该成员⼤⼩的较⼩值。

• VS中默认的对⻬数为8

• 结构体总⼤⼩为:最⼤对⻬数(所有变量类型最⼤者与默认对⻬参数取最⼩)的整数倍。

• 如果嵌套了结构体的情况,嵌套的结构体对⻬到⾃⼰的最⼤对⻬数的整数倍处,结构体的整体⼤⼩ 就是所有最⼤对⻬数(含嵌套结构体的对⻬数)的整数倍。

对齐数是可以通过pack进行修改的 具体的操作可以参考我的文章:自定义类型--结构体

// 实例化的对象是多⼤?

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;//8

cout << sizeof(b) << endl;//1

cout << sizeof(c) << endl;//1

cout << &b << endl;

cout << &c << endl;

return 0;

}

/*

A中起码有成员变量

但是B和C中没有成员变量

那么这种就是属于特殊情况了

没有成员变量的大小不应该是0吗?

但是我们如果是0个字节的话

那么B和C就应该没有地址的

对于没有成员变量的类呢

我们是需要进行开空间的操作

开一个字节的空间

目的是为了占位

不存储有效的数据

标识对象的存在

被迫开了一个字节的空间

表示它们存在过

*/

我们计算这个类的对象的这个大小可以参考结构体的大小的计算

上⾯的程序运⾏后,我们看到没有成员变量的B和C类对象的⼤⼩是1,为什么没有成员变量还要给1个 字节呢?因为如果⼀个字节都不给,怎么表⽰对象存在过呢!所以这⾥给1字节,纯粹是为了占位标识 对象存在。

对于没有成员变量的类呢

我们是需要进行开空间的操作

开一个字节的空间

目的是为了占位

不存储有效的数据

标识对象的存在

对象中不存储成员函数的指针

成员函数的指针在一个公共的区域

3.this指针

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赋值, >year = year; this

• C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会处理),但是可以在函数体内显 ⽰使⽤this指针。

解释这个隐含的this指针

class Data

{

public:

//void Init(Data*const this ,int year, int month, int day)

void Init(int year, int month, int day)

{

_year = year;

_month = month;

_day = day;

}

//void Print(Data* const this)

void Print()

{

//cout << this->_year << "/" << this->_month << "/" << this->_day << endl;

cout << _year << "/" << _month << "/" << _day << endl;

}

private:

//声明 系统在这里没有给变量开空间,那么这里就是声明

int _year;

int _month;

int _day;

};

int main()

{

Data d1;

Data d2;

d1.Init(2024, 6, 5);

//d1.Init(&d1,2024, 6, 5);

d2.Init(2024, 9, 8);

// d1.Init(&d2,2024, 6, 5);

d1.Print();

//d1.Print(&d1);

d2.Print();

//d1.Print(&d2);

return 0;

}

/*

我们在类中定义的函数的参数其实是4个参数

有一个隐参数,是一个this指针

而且被const修饰了

this自己是不能进行改变的

因为这里存在4个参数

那么我们在调用参数的时候那么第一个参数就是传的就是对象的地址

这个就是为什么函数在被调用的时候能进行对象的区分的原因了

而且我们的这个类函数中的打印函数里面的代码

类成员变量的访问本质都是通过this指针进行访问的

cout << this->_year << "/" << this->_month << "/" << this->_day << endl;

这个this指针是隐含的

我们是不需要在实参和形参内写这个this指针

编译器自己会处理的

*/

这个this指针的作用本质还是给函数传对象的地址,然后根据这个对象进行区分数据

小题目

1.下⾯程序编译运⾏结果是(C)

A、编译报错 B、运⾏崩溃 C、正常运⾏

#include<iostream>

using namespace std;

class A

{

public:

void Print()

{

cout << "A::Print()" << endl;

}

private:

int _a;

};

int main()

{

A* p = nullptr;

p->Print();

return 0;

}

//成员函数的指针是在编译时确定的,没有存在对象中,所以这里虽然写了p->

// 但是没有解引用

//空指针的错误是不会报编译错误的

//我们这里传的是空指针,函数接收的是个空指针,结果正常运行

//将p的地址传递给了this

2.下⾯程序编译运⾏结果是(B)

A、编译报错 B、运⾏崩溃 C、正常运⾏

#include<iostream>

using namespace std;

class A

{

public:

void Print()

{

cout << "A::Print()" << endl;

cout << _a << endl;

}

private:

int _a;

};

int main()

{

A* p = nullptr;

p->Print();

return 0;

}

//这里正常将p的地址传给this指针

//因为存在this指针

//我们这里的this指针是空的

//我们在第一句的打印是没有问题的

//但是我们在第二句就会出现错误了

// 原本是下面那样的

//cout << _a << endl;

//实际是这样的

//cout <<this-> _a << endl;

//这里就会解引用

// this指针解引用访问对象中的成员变量

//但是空指针是不能进行解引用的操作的

//那么这个程序就会崩溃的

这两个题的区别就是在Print函数中是否有cout << _a << endl;

如果我们的this指针接收的是空指针的话

如果函数代码中存在打印成员变量的代码的话

多半会报错的

因为这个this指针会通过箭头隐含在代码中

这个this指针会指向这个变量进行解引用

但是这里的是空指针,空指针是不能进行解引用的操作的

所以这里是会报错的

this指针存在内存哪个区域的(A)

A. 栈 B.堆 C.静态区 D.常量区 E.对象⾥⾯

this是一个隐含的形参

函数调用要建立栈帧

那么局部变量要存在栈帧中

我们的形参也要存在栈帧里面

那么这个题就是A

4.C++和C语⾔实现Stack对⽐

⾯向对象三⼤特性:封装、继承、多态,下⾯的对⽐我们可以初步了解⼀下封装。

通过下⾯两份代码对⽐,我们发现C++实现Stack形态上还是发⽣了挺多的变化,底层和逻辑上没啥变化。

• C++中数据和函数都放到了类⾥⾯,通过访问限定符进⾏了限制,不能再随意通过对象直接修改数 据,这是C++封装的⼀种体现,这个是最重要的变化。这⾥的封装的本质是⼀种更严格规范的管 理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后⾯还需要不断的去学习。

• C++中有⼀些相对⽅便的语法,⽐如Init给的缺省参数会⽅便很多,成员函数每次不需要传对象地 址,因为this指针隐含的传递了,⽅便了很多,使⽤类型不再需要typedef⽤类名就很⽅便

• 在我们这个C++⼊⻔阶段实现的Stack看起来变了很多,但是实质上变化不⼤。等着我们后⾯看STL 中的⽤适配器实现的Stack,⼤家再感受C++的魅⼒。

C语言的数据和方法是分离的,数据是数据,方法是方法

C++将数据和方法封装到一起,将数据和方法都封装到类里面,并且通过这个访问限定符进行管理

封装本质是一种规范的管理

C++是不能访问数据的,因为有访问限定符的存在,只能调用函数

对两种语言实现的Stack可以很清楚看出两个语言的不同之处

有点以及缺点



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。