【C++】—— 类与对象(一)
1、类的定义
1.1、类定义
1.1.1、类定义格式
c l a s s class class 为定义类的关键字, c l a s s class class 后接类的名字(自己定),{ } 为类的主体,注意类定义结束时,后面的 ; 不能省略
类体中的内容为类的成员:类中的变量称为类的成员变量或属性,类中的函数称为类的成员函数或方法
下面我们简单写一个栈类来感受一下
class Stack
{
public:
//成员函数
void Init(int n = 4)
{
_a = (int*)malloc(sizeof(int) * n);
if (nullptr == _a)
{
perror("malloc fail");
exit(1);
}
_capccity = n;
_top = 0;
}
void Push(int x)
{
// ...扩容
_a[top++] = x;
}
void Destroy()
{
fail(_a);
_a = nullptr;
_capacity = 0;
_top = 0;
}
private:
//成员变量
int* _a;
size_t _capacity;
size_t _top;
};//分号不能省略
C++中,并没有规定成员函数和成员变量的位置,只要他们在类中就行,把成员变量混在几个函数的中间也是可以的。但一般来讲都是成员函数在上,成员变量在下。
1.1.2、成员变量的标识
- 为了区分成员变量,一般习惯上成员变量会加上一个特殊的标识,如成员变量前或后加上 _ ;或 m ( m e m b e r ) m(member) m(member) 开头;或 m m m_ 开头。这点C++并没有明确规定,具体看公司的要求
在声明栈类的成员变量时,大家可能发现我在变量名前都加上了 “_”,为什么要这么做呢?
我们来看一个日期类:
class Date
{
public:
void Init(int year, int month, int day)
{
year = year;
month = month;
day = day;
}
private:
int year;
int month;
int day;
};
可以看到,上述日期类的成员变量与
I
n
i
t
Init
Init 函数中的形参无法区分,所以为了区分成员变量,要在成员变量前加上特殊的标识
1.1.3、C++ 中的 s t r u c t struct struct
- C++ 中 s t r u c t struct struct 也可以定义类,C++ 中兼容了 C语言 中 s t r u c t struct struct 的用法,同时 s t r u c t struct struct 升级成了类。
- 明显的变化是: s t r u c t struct struct 中可以定义函数。 s t r u c t struct struct 和 c l a s s class class 定义类只有一点细微的差别(下面会说),但一般情况下我们还是推荐使用 c l a s s class class 定义类
在C语言,我们定义一个链表的节点,往往是这样定义的:
typedef struct ListNodeC
{
int val;
struct ListNode* next;
}ListNodeC;
在 C++中,
s
t
r
u
c
t
struct
struct 升级成了类
- 类可以
定义函数
名称就可以代表类型
,不需要 s t r u c t struct struct + 名称
// 不再需要typedef,ListNodeCPP就可以代表类型
struct ListNodeCPP
{
void Init(int x)
{
next = nullptr;
val = x;
}
ListNodeCPP* next;
int val;
};
当然,C++ 是兼容 C 的,所以上面的那种方式 C++ 也支持
1.1.4、C++ 中的内联函数
- 在类中定义的成员函数默认是 i n l i n e inline inline,而如果进行声明和定义的分离:声明在类中;定义在类外,则不然。
1.1.5、总结
- c l a s s class class 为定义类的
关键字
, S t a c k Stack Stack 为类的名字
,{ } 中为类的主体
,注意类定义结束时后面分号不能省略。类体中内容称为类的成员
:类中的变量称为类的属性
或成员变量
;类中的函数称为类的方法
或者成员函数
- 为了区分成员变量,一般习惯上成员变量会
加一个特殊标识
,如成员变量前面或者后面加 _ 或者 m m m 开头,注意 C++ 中这个并不是强制的,只是一些管理,具体看公司要求
- C++ 中 s t r u c t struct struct 也可以定义类,C++ 兼容 C 中 s t r u c t struct struct 的用法,同时 s t r u c t struct struct 升级成了类,明显的变化是 s t r u c t struct struct 中可以定义函数,一般情况下我们还是
推荐使用 class 定义类
- 定义在类里面的成员函数默认为 i n l i n e inline inline
1.2、访问限定符
可能有小伙伴注意到了,前面定义栈类和日期类时,出现了 p u b l i c public public 和 p r i v a t e private private 两个关键字,他们有什么用呢?我们一起来看看
- C++ 一种实现封装的方式,用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
- p u b l i c public public 修饰的成员在类外可以直接被访问; p r o t e c t e d protected protected 和 p r i v a t e private private 修饰的成员在类外不能直接被访问。现阶段我们认为 p r o t e c t e d protected protected 和 p r i v a t e private private 是一样的,以后集成章节才能体现出他们的区别.
- 访问权限作用域从该访问限定符
出现
的位置开始
,直到下一个
访问限定符出现为止
,如果后面没有访问限定符,作用域就到} (收括号)
即类结束- c l a s s class class 定义成员没有被访问限定符修饰时
默认
为 p r i v a t e private private, s t r u c t struct struct默认
为 p u b l i c public public 。这也是 c l a s s class class 和 s t r u c t struct struct 的唯一区别- 一般成员变量都会被限制为 p r i v a t e private private/ p r o t e c t e d protected protected,需要给别人使用的成员函数为 p u b l i c public public。当然这只是一般情况,并没有硬性规定
举个栗子:
当然,一般情况下也没有人会这样写。一般都是公有的放一起,私有的放一起。
像这样:
1.3、类域
在C++中,凡是用 { }
括起来的都会形成一个 域。类也不例外,类定义了一个新的作用域: 类域,类的所有成员都在类的作用域中。
不知大家有没有注意到,在前面定义的栈类中,成员函数的命名不再像之前 C语言 写栈时那样要加上栈的标识,如: void STDestroy()
。之前 C语言 这样写是因为结构体和函数是分离的
,栈叫
D
e
s
t
r
o
y
Destroy
Destroy ,队列也叫
D
e
s
t
r
o
y
Destroy
Destroy 就有可能搞混
,且同一个域中也不允许出现同名的变量或函数。
现在他们是栈类的成员函数,在类域之中,即使后面定义的一个队列域有同名的成员函数,也不会冲突。因为名字冲突只发生在同一个域中,不同域可以有相同名字
那当成员函数的声明和定义分离时,即声明在类内,定义在类的外面,又该如何定义函数呢?
class Date
{
public:
void Init(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
我们知道,任何一个变量,编译器都会去找他的出处
(声明/定义)。编译器默认只会在全局域或当前函数局部域去找,并不会去类域中找。那怎么办呢?我们告诉他去指定的类域中找就行了。
- 在类体外定义成员时,需要使用 : : 作用域操作符 指明成员属于哪个类域
class Date
{
public:
void Init(int year, int month, int day);
private:
int _year;
int _month;
int _day;
};
void Date::Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
这样就行了
- 类域影响的是编译的查找规则
上述程序 I n i t Init Init 如果不指定类域 D a t e Date Date,那么编译器就把 I n i t Init Init 当成全局函数,那么编译时,找不到 _ y e a r year year 等成员的声明/定义在哪里,就会报错。
指定类域 D a t e Date Date,就是告诉编译器 I n i t Init Init 是成员函数,在当前域找不到的 _ y e a r year year 等成语,就会到类域中去查找。
2、实例化
2.1、实例化的概念
在讲解类的实例化之前,我们先来思考一个问题
上述成员变量是声明还是定义呢?
答案是 声明
对变量来说,到底是声明还是定义是看他是否有开空间:开了空间的是定义;没开空间的是声明
int main()
{
Date::_year = 0;
return 0;
}
我们直接这样访问是会报错的,因为 _
y
e
a
r
year
year 只是一个声明
,并没有开空间。
那什么时候开空间呢?
用该类型实例化出一个对象才是开空间
int main()
{
Date d1;
Date d2;
Date d3;
return 0;
}
上述就是类的实例化,类和对象是一对多
的关系,一个类可以实例化出多个对象
怎么理解这个实例化呢?我们可以用图纸和实物建筑来类比
类就相当于图纸,图纸上有房子上的各种信息,当不能进去住人;而实例化对象就像是依照着图纸将房子盖好,盖好的房子是可以住人的,相当于实例化开了空间。
2.2、对象大小
2.2.1、对象的大小包括什么
在学习 C语言 结构体时,我们知道结构体中就存着成员,这些成员要按照内存对齐的规则去进行计算大小。类的对象中的成员变量也是如此,成员变量是存储在对象中的。
现在的问题是:成员函数是否是存储在对象中的呢?
答案是:不需要
为什么呢?我们用日期类来举例
class Date
{
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 main()
{
// Date类实例化出对象d1和d2
Date d1;
Date d2;
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
上述代码中,示例化出了
d
1
d1
d1 和
d
2
d2
d2 两个对象
我们给两个对象的年月日(成员变量)初始化了不同的值,所以他们的成员变量是不一样的
,他们要各种存储自己的成员变量
,因此成员变量肯定是存储在对象中
那现在的
d
1
d1
d1 和
d
2
d2
d2 调用
P
r
i
n
t
Print
Print 函数是调用同一个函数还是不同的函数?
很显然是同一个函数
既然都是一样的,那在各自的对象中都存一份,是不是太浪费空间
了。我要是实例化10000个对象那不是要重复存储10000份?
我们通过汇编代码可以看到两个对象调用的都是同一个函数
两个 c a l l call call 指令, c a l l call call 的地址都是一样的,表明跳转的是同一个函数
c a l l call call 指令跳转到 jmp 指令
j m p jmp jmp 指令再跳转到函数
成员函数其实是存在一个公共的区域
既然函数不存在对象里面,那函数指针
有必要存在对象里面吗?
也是没必要的,原因还是重复存储浪费空间。
- 函数指针是⼀个
地址
,调⽤函数被编译成汇编指令[ c a l l call call 地址],其实编译器在编译链接
时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在运⾏时找,就需要存储函数地址,这个我们以后再学习
2.2.2、内存对齐规则
类的大小计算和结构体的计算是一样的,这里我们简单回顾一下。详情请看【C语言】——结构体
- 第一个成员在与结构体偏移量为 0 的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值
- VS 中默认的对齐数是 8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到之间的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(函嵌套结构体的对齐数)的整数倍
2.2.3、空类的大小
class A
{
public :
void Print()
{
//...
}
};
int main()
{
A a;
cout << sizeof(a) << endl;
return 0;
}
现在有一个问题:
a
a
a 的大小是多大呢?
运行结果:
为什么该对象没有成员变量
还有 1 字节的大小呢
这里的 1 为了占位
因为如果一个空间对不给,怎么证明这个对象存在过呢,所以这里给了1个空间大小,纯粹是为了占位表示对象存在
3、 t h i s this this 指针
class Date
{
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 main()
{
Date d1;
Date d2;
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
现在,我们知道
d
1
d1
d1 和
d
2
d2
d2 调用的都是同一个
I
n
i
t
Init
Init 函数和
P
r
i
n
t
Print
Print 函数,那既然是同一个函数为什么能打印出各自的成员变量呢
?他们是怎么区分
d
1
d1
d1 和
d
2
d2
d2 的呢?
C++中,给了一个隐含的
t
h
i
s
this
this指针 解决这个问题
- 编译器编译后,类的成员函数默认会在形参的第一个位置,增加一个
当前类类型的指针
,叫做 t h i s this this指针。比如 D a t e Date Date 类的 I n i t Init Init 的真实原型为:void Init(Date* const this, int year,int month, int day)
- 类的成员函数中访问成员变量,本质是通过 t h i s this this 指针访问的,如 I n i t Init Init 函数中给 _ y e a r year year 赋值,
this->_year = year;
- C++ 规定
不能在实参和形参的位置显示的写
t h i s this this指针(编译时编译器会处理),但是可以在函数体内显示使用
t h i s this this 指针
因此,上述代码底层是这样的:
class Date
{
public:
void Init(Date* const this, year, int month, int day)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
void Print(Date* const this)
{
cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(&d1, 2024, 3, 31);
d1.Print(&d1);
d2.Init(&d2, 2024, 7, 5);
d2.Print(&d2);
return 0;
}
注:这只是底层,实际代码是不允许这样写的。因为
t
h
i
s
this
this 指针不能在实参和形参显示写
;但在函数体内可以使用 this 指针
那么
t
h
i
s
this
this指针是存在哪一个区域的呢?
首先,肯定不是对象里面。因为我们前面讲解对象大小时,并没有计算
t
h
i
s
this
this 指针
我们来看,
t
h
i
s
this
this 指针是一个形参,那形参是存在哪里的呢?
答案是:栈
但这答案也不完全对。
因为
t
h
i
s
this
this 指针需要频繁调用
,因此有些编译器(如VS)会对其进行优化,放在寄存器
中
4、练习
4.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;
}
首先把 A 排除了,因为上述程序就算出错那也是因为空指针
的问题出错,对空指针的解引用什么时候是编译报错呢?
这题的答案:B
我们来看
p->Print();
,这句代码转换成汇编指令
是: c a l l call call 指令 -> j m p jmp jmp 指令 -> P r i n t Print Print函数;那 P r r i n t Prrint Prrint 函数在哪呢?在公共代码
区,并不在对象中;同时 P r i n t Print Print函数需要传递 t h i s this this 指针,这里传的是 p p p,即 n u l l p t r nullptr nullptr。
这一切都没有问题。虽然传递了 n u l l p t r nullptr nullptr 给 t h i s this this 指针,但是函数内并没有对其进行解引用
我们不要看到对象调用函数:p->Print();
就以为是对 n u l l p t r nullptr nullptr 进行了解引用,我们要关注代码的底层
那为什么需要对象取调用呢?因为 P r i n t Print Print 是其成员函数,在其类域之中,它要过编译那关,语法那关
4.2、题二
下面程序的运行结果是?
A、编译报错 B、正常运行 C、运行崩溃
class A
{
public :
void Print()
{
cout << "A::Print()" << endl;
cout << _a << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
答案:C
这题与上题的区别是
P
r
i
n
t
Print
Print 函数中多了cout << _a << endl;
前面的步骤都是与上题一样,但到了cout << _a << endl;
语句时,要对
t
h
i
s
this
this 指针进行解引用cout << this->_a << endl;
,因为
t
h
i
s
this
this 指针是空指针,对空指针进行解引用自然就运行崩溃啦
好啦,本期关于类和对象的知识就介绍到这里啦,希望本期博客能对你有所帮助。同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!