目录
2.1.区分什么时候是成员函数定义、声明,什么时候是成员变量定义、声明
1.1.存储方式1:对象内存空间包含类的各个成员(即成员变量、成员函数)
1.2.存储方式2:代码只保存一份,在对象中保存存放代码的地址
1.3. 存储方式3:对象内存空间只保存成员变量,成员函数存放在公共的代码段
2.1.从类对象大小的计算结果可知成员函数是不存放在类对象中的,那类的成员函数是存放在什么地方?
3.3.解析用仅有成员函数的类、空类来定义对象时对象时对象大小只有1字节的原因
3.2.通过空指针this访问成员变量、成员函数是否一定会发生报错?
一、面向过程和面向对象初步认识
注意:C++语言并不是一种纯面向对象的语言,与Java这种纯面向对象的语言不同。由于C语言是面向过程的语言,而C++语言也兼容C语言使得C++语言既是面向对象也是面向过程的语言。
1.面向过程介绍
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
总的来说,面向过程关注的是解决问题的步骤和过程。
图形解析:下面展示了洗衣服这个问题的步骤和过程
2.面向对象
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完
成。
二、类的引入
1.可以利用关键字struct
来定义类的原因
在C语言中,结构体(struct)只能包含成员变量(数据),不能包含成员函数(方法)。而在C++中,结构体不仅可以包含成员变量(数据),还可以包含成员函数(方法),这使得C++中的结构体与类(class)非常相似,所以C++可以使用关键字struct来定义类。
1.1.C++可以使用struct
来定义类的原因是
-
C++的向后兼容性:C++语言在设计时考虑到了对C语言的兼容性。因此,C++保留了C语言的大部分语法和结构,包括
struct
关键字。 -
struct
的扩展:在C++中,struct
不仅用于定义复合数据类型,还被赋予了更多的面向对象的特性。C++中的struct
与class
的主要区别在于默认的访问权限:在struct
中,成员(即成员变量、成员函数)默认是公有的(public),而在class
中,成员(即成员变量、成员函数)默认是私有的(private)。 -
语法上的简化:允许使用
struct
来定义具有成员函数的类,为C++程序员提供了更多的灵活性。在某些情况下,当不需要复杂的封装时,使用struct
可以简化代码。
因此,C++中的struct
实际上是一种轻量级的类,除了默认的访问权限不同之外,它几乎具有类所有的特性,包括成员函数、构造函数、析构函数、继承、多态等。这使得C++的struct
可以像类一样定义对象的行为和数据。
2.利用关键字struct
定义类及访问类成员的案例
注意:①由于类的成员包括成员变量、成员函数,所以当我们要访问类的成员时主要访问的是类的成员变量和成员函数。②下面只是浅浅的说明如何利用关键字struct
定义类,所以下面类Stack的成员函数的定义实现是没有写全的。
//cpp
#include <iostream>
using namespace std;
//在C++中,我们可以使用关键字struct来定义一个类,这和用class关键字定义类几乎一样,除了成员的默认访问权限不同。
struct Stack
{
//解析:
//1.成员函数的定义:成员函数可以在类内部直接定义,也可以在外部定义。在类内部定义的成员函数默认是内联函数的。
//2.成员函数的调用:与C语言不同,定义在C++类中的成员函数在调用另一个成员函数时不需要提前声明函数后才能调用。
//这是因为成员函数属于类的内部,编译器会在类定义中查找这些函数的定义。
//总的来说,成员函数在类中定义的位置不会影响其在类中的调用。
//3.函数名的命名:
//3.1.在C语言中,由于没有类的概念,所有的函数都是全局作用域的。所以为了明确全局域中定义的函数属于那个数据结构,通常会使用前缀(即数据结构的名称)来命名函数。
//例如在C语言全局域中定义的插入函数的函数名必须写成StackPush,则此时我们才知道我这个插入函数是对栈这个数据结构进行插入的。
//3.2.在C++中,由于成员函数是类的一部分而且我们只能通过类对象来访问成员函数,所以不需要前缀来指明它们属于那个类(注;这个类可以是数据结构),而
//成员函数的命名直接反映了它们的功能.
//例如,一个用于向栈中插入元素的成员函数可以直接命名为 Push,而一个用于初始化栈的函数可以命名为 Init。这样的命名方式使得代码更加简洁和直观。
//初始化函数,带有缺省参数,用于分配栈空间。
void Init(int n = 4)//缺省参数
{
a = (int*)malloc(sizeof(int) * n);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
size = 0;
}
//插入函数,用于向栈中添加元素。
void Push(int x)
{
//扩容检查...省略
//插入数据
a[size++] = x;
}
//成员变量:用于存储栈的元素、大小和容量。
int* a;
int size;
int capacity;
};
//在main函数中使用Stack类
int main()
{
//访问类Stack成员的方式和访问结构体成员的方式是一样的:通过'.'操作符进行成员访问
Stack st; //创建一个Stack类的实例
//当我们不知道栈st存放多少数据时,则我们不传参而是而是直接用Init中缺省参数的值n = 4把栈st初始化为大小为4的栈st。
st.Init(); //调用初始化函数,使用缺省参数的值进行初始化。
//对栈st进行插入的过程:
st.Push(1);
st.Push(2);
st.Push(3);
return 0;
}
// 注意:尽管C++兼容C语言,并且可以使用struct定义类,但在C++中更常见的做法是使用关键字class来定义类。
3.使用关键字struct定义结构体、定义类的区别
3.1.C语言的结构体类型定义
typedef struct ListNode
{
int val;
struct ListNode* next;
}LTN;
解析: 在这个定义中,struct ListNode
是一个结构体类型,它包含一个整型变量 val
和一个指向相同结构体类型的指针 next
。使用 typedef
为这个结构体类型创建了一个新的名字 LTN
,这样就可以直接使用 LTN
来声明结构体变量,而不需要每次都写 struct ListNode
。
3.2.C++的类定义
struct ListNode
{
int val;
ListNode* next;
};
解析:在C++中,struct
关键字不仅可以用来定义结构体,也可以用来定义类。这里的 ListNode
被定义为一个类,它包含一个整型成员变量 val
和一个指向 ListNode
类型的指针 next
。在C++中,不需要 typedef
来简化类型的声明,可以直接使用类名 ListNode
来声明指针。
3.3.区别
(1)类型区别
- C语言的结构体类型:使用
struct ListNode
来声明结构体变量。 - C++中的类的类型:直接使用
ListNode
来声明类的对象或指针。
(2)成员区别、成员命名区别
- 在C++中,类可以包含成员变量和成员函数,而C语言的结构体只能包含变量。这是类和结构体在C++中的一个主要区别。
- 在C语言中,当我们没有使用
typedef
关键字来为结构体类型创建一个新的名称时,我们必须使用struct
关键字加上结构体标签名来声明和定义变量或指针。在C++中,使用struct
关键字定义的类型可以直接使用类型名(即标签名)来声明变量或指针,而不需要额外的typedef
声明。例如:当我们用struct定义类时,类成员变量的类型可以直接使用类名(标签名)来声明,不需要额外的typedef
声明。这是因为在C++中,struct
和class
关键字几乎相同,除了默认的访问权限不同(struct
的成员默认是公有的,而class
的成员默认是私有的)。
三、类的定义
1.类的2种定义格式
注意:由于C++兼容C语言,所以C++有两种定义类的方式,第1种是利用关键字struct来定义类,第2种是利用关键字class定义类。这里我主要讲如何利用关键字struct来定义类的格式。
类的两种定义方式(注意:C++不太喜欢使用struct定义类,而是用class定义类):
(1)利用class定义类
class className
{
//类体:由成员函数(方法) 和 成员变量(属性) 组成。
}; //在定义类时一定要注意写后面的分号。
//对类的解析:
//1.class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分
//号不能省略。注意:类的名称ClassName就是类的类型,而用类的名称(即类型)定义的变量在C++中叫做对象。
//2.类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者
//成员函数。
//3.在C++类定义中,成员变量和成员函数的声明顺序确实是无关紧要的,只要满足以下条件:
//3.1.在成员函数的定义(实现)中访问另一个成员函数或成员变量之前,该成员函数或成员变量必须已经被声明。
//3.2.成员函数可以在类体内部或外部定义,但如果在类体内部定义,则它们默认是内联的。
//3.3.如果成员函数在类体外部定义,则它们必须在使用之前在类体内部声明。
//4.在C++中,类定义中的成员函数可以在类体内部的任何位置声明,但是在同一个成员函数的实现中调用
//另一个成员函数或者在成员函数中使用成员变量之前,被调用的成员函数或使用的成员变量必须已经被
//声明(注:声明位置在类中是任意位置)。
(2)利用struct定义类
struct className
{
//类体:由成员函数(方法) 和 成员变量(属性) 组成。
};
//注意:className为类名,而且className是类的类型
2.定义类的2种方式
方式1:利用struct定义类的2种方式
2.1.声明和定义在一起的类
解析:声明和定义在一起的类指的是成员函数直接定义在内里面去。若成员函数在类中定义,编译器可能会将其当成内联函数来处理。
(1)案例
//Stack.cpp
//类的成员函数直接定义在类中
struct Stack
{
//1.成员函数
void Init(int n = 4)//缺省参数
{
a = (int*)malloc(sizeof(int) * n);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
size = 0;
}
void Push(int x)
{
//扩容检查...省略
a[size++] = x;//插入数据
}
//2.成员变量
int* a;//数组
int size;//存放数据个数
int capacity;//容量
};
2.2.声明和定义分离的类
解析:声明和定义分离的类指的是成员函数没有定义在类中。成员函数的声明放在.h文件中,成员函数定义放在.cpp文件中(注意:当在源文件定义成员函数时成员函数一定要在函数名前用作用域限定符'::'来指定成员函数对应的类,否则会发生报错)
类的声明和定义分离代码(正确写法):
2.2.1.h(头文件)声明类
//Stack.h
#include <stdlib.h>
//类成员函数声明和定义分离
struct Stack
{
//1.成员函数
void Init(int n = 4);//缺省参数
void Push(int x);
//2.成员变量
int* a;//数组
int size;//存放数据个数
int capacity;//容量
};
2.2.2.cpp(源文件)定义类
//Stack.cpp
#include "Stack.h"
//在Init的前面使用Stack::的目的是告诉编译器这个Init函数不是全局域中定义的函数而是在类Stack定义的成员函数。
//注意:缺省参数不能在声明和定义同时给缺省值,只能在声明(头文件)中给缺省值,否则会发生报错,所以这里定义的Init函数中的参数没有给缺省值。
void Stack::Init(int n)
{
//用域作用限定符'::'给Init函数指定作用域类Stack的目的是:由于类Stack中的成员变量和成员函
//数是一体的,只有给Init函数指定类Stack才能让Init函数的内部访问到类Stack的成员变量指针a。
a = (int*)malloc(sizeof(int) * n);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
size = 0;
}
//在Push的前面使用Stack::的目的是告诉编译器这个Push函数不是全局域中定义的函数而是在类Stack定义的成员函数。
void Stack::Push(int x)
{
//...
a[size++] = x;
}
2.2.3.struct定义类的使用
int main()
{
//访问类Stack中成员的方式和结构体访问成员的方式是一样的:通过'.'操作符进行成员访问。若是
//指向类的指针Stack* pst则要通过'->'操作符来访问类Stack的成员。
//类Stack的实例化:定义类Stack的对象st
Stack st;//定义一个栈st
//对栈st进行初始化
st.Init();//但我们不知道栈st初始化为多少(即栈st不知道存放多少数据)时,则我们就不对初始化
//函数Init进行传参而是直接用Init中缺省参数的值n = 4把栈st初始化为大小为4的栈st。
//对栈st进行插入的过程:
st.Push(1);
st.Push(2);
st.Push(3);
return 0;
}
类声明和定义分离错误写法的案例:
案例1
错误原因:由于类中的成员函数和成员变量可以互相调用的,但是在类定义时没有给成员函数Init、Push利用 ‘::’ 操作符来指定类Stack使得Init、Push函数默认是在全局域中定义而不是在类Stack中定义,但是在全局域中定义的函数Init、Push的内部没有定义变量而是直接使用类Stack中的成员变量(例如:Init函数内部变量指针a、capacity、size并没有被定义创建;Push函数内部变量指着a、size也没有定义创建),而且由于类Stack的成员没有指定访问限定符使得类Stack的成员(成员函数、成员变量)默认是私有的,进而使得程序报错。
案例2
错误原因:缺省参数在声明和定义同时给缺省值造成缺省参数重定义。解决方式是我们一般只在声明时给缺省参数指定缺省值而定义时是不给缺省参数指定缺省值的。
方式2:利用class定义类的2种方式
2.3.声明和定义在一起的类
声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内
联函数处理。
2.4.声明和定义分离的类
类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
注意:我们平时一般使用声明和定义分离方式来定义类。
四、类的访问限定符
1.访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选
择性的将其接口提供给外部的用户使用。
1.1.访问限定符说明
1.1.1.注意事项
(1)在 C++ 中,访问限定符(如 public
、private
和 protected
)是用于定义类成员的访问级别,这些限定符的作用是在编译时由编译器进行检查和强制执行的。但是,一旦程序编译完成并且运行时,这些访问限定符不会以任何方式影响内存中的数据布局或访问方式。
-
编译时检查:访问限定符在编译阶段由编译器检查,以确保类的使用者只能通过定义的接口访问类的成员。例如,如果某个成员被声明为
private
,则它不能从类的外部直接访问。
(2)访问限定符不限制类里面的成员变量和成员函数之间的相互调用。访问限定符只是限制类外面对类里面的成员变量和成员函数的访问权限。即可以把访问限定符可以看成是一把锁,这把锁是锁外面的人,但是不锁里面的人。
解析:访问限定符(public
、private
和 protected
)在 C++ 中用于控制类成员的访问级别。这些限定符的主要目的是限制从类外部对类成员的访问,而不是限制类内部成员之间的相互调用。
public
:标记为public
的成员可以在类的内部和外部被访问。private
:标记为private
的成员只能在类的内部被访问,不能被类的外部访问。protected
:与private
类似,但protected
成员可以被类的派生类访问。
总结来说,访问限定符确实是“锁外面的人”,允许类内部的成员自由交互,而不会“锁里面的人”。
(3)C++一共有3中访问限定符。一般认为保护和私有没有什么区别,只有在继承的时候保护和私有才有区别。
1.1.2.访问限定符说明
(1)public修饰的成员在类外可以直接被访问
解析:公有限定符public是类外面对类里面的成员变量、成员函数进行随意访问。
(2)protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
解析:保护访问限定符protected和私有访问限定符private是类外面不能随意对类里面的成员变量、成员函数进行访问。
(3)访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到 } 即类结束。
解析:一个类中可以有多个访问限定符。当一个类有多个访问限定符时候,一个访问限定符的作用域是从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,若没有下一个访问限定符则该访问限定符的作用域一直到 } 就结束。(注意:作用域就是访问限定符起作用的范围)
图形解析:报错原因是由于类Stack中有两个访问限定符而且每个访问限定符有自己的作用域则当我们在main函数中访问类Stack中的成员函数、成员变量时由于类Stack的成员函数是公有public而成员变量是私有private的使得在main函数中访问成员函数没有发生报错但是当我们访问成员变量时会发生报错。
(4)class的默认访问权限为private,struct为public(因为struct要兼容C)
解析:在C++中用struct或者用class都可以定义类。
①若用struct定义类时没有用访问限定符保护private或者私有private 限制类外面对类里面的成员变量、成员函数的访问权限,则struct默认类的所有成员是公有public,进而使得类外面可以随意访问struct定义类中的所有成员变量、成员函数。
②若用class定义类时没有用访问限定符公有public 允许类外面对类里面的成员变量、成员函数进行随意访问的话,则class默认类的所有成员是私有private进而使得类外来不可随意访问类的所有成员。
案例:
- 案例1:类Stack使用public访问限定符
- 案例2:报错原因是class的默认访问权限为private使得我们不能类外面随意访问类Stack中的成员变量、成员函数。
(5)在实际写类当中,我们是不期望用默认权限写类的,而是类的成员函数是公有public,而成员变量是私有private。
解析:在实际编写类时,应该明确地将成员函数声明为公有,以便类外部代码可以与之交互,而将成员变量声明为私有,以保护类的内部状态并实现封装。
2.访问限定符使用案例——利用class定义和声明类
问题:C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来
定义类。struct和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序文章给大家介绍。
2.1.声明类
//Stack.h
#include <stdlib.h>
//类成员函数声明和定义分离
//一个类大多数情况下,类是不希望别人改变类中成员变量的数据,而成员函数若是不给别人用时则是私有,给别
//人用时则是公有。即一个类在大多数情况下,成员变量被私有privact限定,而成员函数被公有public限定。
class Stack
{
public://公有
//1.成员函数
void Init(int n = 4);//缺省参数
void Push(int x);
private://私有
//2.成员变量
int* a;//数组
int size;//存放数据个数
int capacity;//容量
};
(1)在面向对象编程中,通常推荐将类的成员变量设置为私有(private),以隐藏类的内部实现细节,并通过公有的成员函数(public)提供对这些私有成员的访问和控制。详细解析如下:
在面向对象编程实践中,类的设计通常会遵循封装的原则,这意味着类的内部状态(成员变量)应当被隐藏起来,以防止直接从类的外部进行访问和修改。这样做可以保护类的内部状态,确保状态的改变只能通过定义良好的接口(即公有的成员函数)来进行,从而提高代码的可靠性和可维护性。
因此,大多数情况下,类的成员变量应当被声明为私有(private),这样只有类的内部成员函数可以访问和修改这些变量。而成员函数,根据它们是否需要被类的使用者调用,可以被声明为公有(public)或私有(private):
- 如果成员函数提供了类与外界的交互接口,它们应当被声明为公有(public)。
- 如果成员函数是用于类的内部实现,不应该被类的使用者直接调用,它们应当被声明为私有(private)。
2.1.定义类
//Stack.cpp
#include "Stack.h"
//注意:由于类的成员函数的内部有时会调用类的成员变量来使用所以我们在源文件定义类的成员函数时
//一定要利用作用域限定符'::'为成员函数指定类,若没有指定类则在源文件中定义的函数会默认在全局域
//中定义而且由于函数会用到类的成员函数则此时一定会发生报错。
void Stack::Init(int n)
{
a = (int*)malloc(sizeof(int) * n);
if (nullptr == a)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
size = 0;
}
void Stack::Push(int x)
{
//...省略
a[size++] = x;
}
2.3.class定义的类的使用
//main.cpp
#include "Stack.h"
#include<iostream>
using namespace std;
int main()
{
//访问类Stack中成员的方式和结构体访问成员的方式是一样的:通过'.'操作符进行成员访问
Stack st;//定义一个栈st
//class Stack st;//但是这种写法一般很少用
//对栈st进行初始化
st.Init();//但我们不知道栈st初始化为多少(即栈st不知道存放多少数据)时,则我们就不对初始化
//函数Init进行传参而是直接用Init中缺省参数的值n = 4把栈st初始化为大小为4的栈st。
//对栈st进行插入的过程:
st.Push(1);
st.Push(2);
st.Push(3);
return 0;
}
3.优化后的class定义的类
3.1.优化原因
(1)案例1:声明和定义分离
错误原因:由于成员函数的参数名capacity和成员变量capacity的命名发生冲突,同时类中的成员变量和成员函数是一体的(即类中的成员变量和成员函数可以互相调用)进而导致我们无法知道成员函数Init内部实现时是用参数的值capacity去malloc开辟空间还是用类成员变量的值capacity去malloc开辟空间。
(2)案例2:声明和定义一起
①错误代码:
②修改后的代码:
//类的声明和定义不分开
class Date//日期类
{
//公有
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//私有
private:
int _year;
int _month;
int _day;
};
3.2.优化后的class定义的类
为了解决上面两个案例中出现的成员变量名和成员函数的参数命名发生冲突问题,则解决方式如下:在类的所有成员变量的变量名的前面都加下划线'_'。
①声明
//Stack.h
#include <stdlib.h>
//类成员函数声明和定义分离
//一个类大多数情况下,类是不希望别人改变类中成员变量的数据,而成员函数若是不给别人用时则是私有,给别人用时则是公有。即
//一个类在大多数情况下,成员变量被保护privact限定,而成员函数被公有public限定。
class Stack
{
public://公有
//1.成员函数
void Init(int n = 4);//缺省参数
void Push(int x);
private://私有
//2.成员变量
//注意:下面成员变量的变量名的前面都加下划线'_'的目的是防止成员变量名和成员函数的参数的命名发生冲突。
int* _a;//数组
int _size;//存放数据个数
int _capacity;//容量
};
②定义
//Stack.c
#include "Stack.h"
void Stack::Init(int n)
{
_a = (int*)malloc(sizeof(int) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_size = 0;
}
void Stack::Push(int x)
{
//...
_a[_size++] = x;
}
五、封装
面向对象的三大特性:封装、继承、多态。
注意:封装指的是把我们下面的细节藏起来。封装是为了更好的管理。C++相对于C语言的封装是把方法(即成员函数)和数据(即成员变量)放进类中,同时类中的内容想给你访问的就定义成公有public,不像给你访问的就定义成私有private。
在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来
和对象进行交互。
封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用
户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日
常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。
对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如
何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计
算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以
及键盘插孔等,让用户可以与计算机进行交互即可。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来
隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
六、类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用‘::’
作用域操作符指明成员(即成员函数)属于哪个类域。
#include <iostream>
using namespace std;
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
七、类的实例化
1.类实例化介绍
(1)类实例化概念:用定义好的类这个类型创建对象的过程,称为类的实例化
(2)类在面向对象编程中扮演着蓝图或模板的角色,它定义了一组具有相同特征和行为的对象的抽象模型。类描述了这些对象将具有的属性(成员变量)和行为(成员函数),但它本身并不占用内存空间来存储具体的对象数据。
详细解析:
-
类的定义:类是一个抽象的概念,它规定了对象应该具有哪些属性(数据成员)和方法(成员函数)。当我们定义一个类时,我们并没有在内存中创建任何具体的数据结构来存储这些属性和方法,而是创建了一个模板,用来生成具有这些属性和行为的对象。
-
类与对象的关系:这可以类比于模具与铸件的关系。类就像是模具,它定义了铸件的形状和结构,而对象就像是实际铸造出来的铸件,是类的具体实例。每个对象都是根据类的定义在内存中分配的,并具有自己的状态(成员变量的值)。
-
类与学生信息表的类比:类就像是一张学生信息登记表,它定义了需要收集哪些信息(如姓名、年龄、性别等),但表本身并不包含任何具体学生的数据。当我们填写这张表格时,我们实际上是在创建一个具体的学生信息记录,这相当于类的实例化过程。
-
类与谜语的类比:类就像是一个谜语,它描述了谜底的特征,但不直接揭示谜底。谜底本身,即谜语的具体实例,才是真正解决问题的答案。在面向对象编程中,类定义了对象的结构和行为,而对象则是类的具体实现。
综上所述,类是对象的抽象描述,定义类并不分配内存,而是定义了创建对象时应该遵循的模板。
(3)一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量
Person类是没有空间的,只有Person类实例化出的对象Person man才有具体的年龄man._age = 10。
(4)做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设
计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象
才能实际存储数据,占用物理空间
2.类实例化案例
2.1.区分什么时候是成员函数定义、声明,什么时候是成员变量定义、声明
//日期类
class Date
{
public:
//对于函数来说,函数是声明还是定义取决于函数的功能是否被实现。若函数的功能被实现则该函数
//就是在定义;若没有实现则该函数就是在声明。总的来说,函数的定义是实现函数的功能、函数的
//声明是告诉编译器存在这个函数。
//下面是成员函数的定义
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
//对于变量来说,变量是声明还是定义取决于变量是否开辟空间了。若变量开辟空间则变量就是定义
//了;若变量没有开辟空间则变量就是声明。例如:在结构体、枚举、联合体、类中的成员变量都是
//变量的声明,因为这些成员变量只是声明并没有开辟空间去定义。
//下面都是成员变量的声明
int _year;
int _month;
int _day;
};
//注:类被定义出来后并不能直接存放数据,而是通过定义类的对象后,然后用对象存放数据。
int main()
{
//类的实例化指的是:给对象开辟空间。
//由于日期类对象d1开辟了空间,则日期类对象d1中的成员变量就开辟空间,则此时的成员变量就是
//定义。
Date d1;
return 0;
}
(1)函数的定义、声明
①声明:函数的声明告诉编译器函数的名称、返回类型以及参数类型,但它不包含函数体。声明可以多次出现,但定义只能出现一次。如果在其他地方使用函数之前没有声明,编译器将无法识别该函数,导致编译错误。
②定义:函数的定义包含了函数的声明以及函数体的实现。在链接阶段,编译器需要找到每个函数的定义,如果没有找到,将出现链接错误。
注:若当前源文件中只有函数声明而整个工程没有函数定义则会使得发生链接错误进而导致报错。
(2)变量的定义、声明
①声明:变量的声明告诉编译器变量的类型和名称,但它不分配内存空间。在类中声明成员变量时,并没有为这些变量分配空间,因此这些声明不会产生内存地址。
②定义:变量的定义不仅声明了变量的类型和名称,还分配了内存空间。
(3)成员函数声明和定义、成员变量声明和定义
①成员变量声明和定义
- 成员变量在类中的声明并不分配内存空间,它们只是告诉编译器这些变量将会存在,并指定了它们的类型。
- 只有当类被实例化,也就是创建了一个该类的对象时,成员变量才会被分配内存空间,这时才算完成了成员变量的定义。
②成员函数声明和定义
- 成员函数的定义包含了函数的返回类型、名称、参数列表以及函数体(实现)。无论成员函数的定义是在类内部还是类外部,只要提供了函数的实现,就完成了成员函数的定义。
- 如果成员函数在类内部定义,则它们默认是内联的,这意味着编译器可能会在每个调用点展开函数体。
- 如果成员函数在类外部定义,则需要在类外部提供函数的实现,通常是在类的实现文件(.cpp 文件)中。在这种情况下,类声明(.h头文件)中提供的是函数的声明,而外部实现则是函数的定义。
2.2.错误访问类成员变量的方式
注意:①没有定义类的对象就直接访问类的成员变量是错误的,因为没有定义对象则成员变量就没有创建空间,没有空间就不能访问成员变量否则会发生报错。
②类定义后,类中的成员变量只是声明却没有定义即没有开辟空间,所以在没有定义类的对象之前不能直接访问类中的成员变量。但是由于类定义后类中的成员函数。
(1)错误案例1:
(2)错误案例2:
八、类对象模型
1.猜测类对象存储方式
注意:对象的内存空间只存放成员变量而不存放成员函数。成员函数的代码是存放在程序的代码段中,这是一个所有对象都可以访问的公共区域。因此,当对象需要调用类中的成员函数时,它不会在对象自己的内存空间中查找该函数,而是直接在代码段中查找并执行相应的函数代码。当对象需要访问成员变量时,它会在自己的内存空间中查找并使用存储在那里的成员变量。
1.1.存储方式1:对象内存空间包含类的各个成员(即成员变量、成员函数)
- 描述:这种存储方式假设每个对象都包含其成员变量和成员函数的代码。
- 缺陷:每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一
个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。 - 对于存储方式1的缺陷,那采用存储方式2是否可以解决?
1.2.存储方式2:代码只保存一份,在对象中保存存放代码的地址
- 描述:这种方式下,成员函数的代码只存储在程序的一个地方(代码段),而每个对象中只保存指向这些函数代码的指针(或地址)。
1.3. 存储方式3:对象内存空间只保存成员变量,成员函数存放在公共的代码段
- 描述:这种方式下,对象的内存空间只包含成员变量,而成员函数的代码存储在程序的代码段中,这是一个共享的、只读的内存区域。
- 这种方式不仅避免了空间浪费,还使得所有对象都可以共享相同的函数实现。
2.证明类对象存储方式是存储方式3
解析:从类对象d1大小的计算结果可以看出对象d1中只存放成员变量但没有存放成员函数。
2.1.从类对象大小的计算结果可知成员函数是不存放在类对象中的,那类的成员函数是存放在什么地方?
(1)解析对象只存放类成员变量而不存放成员函数的原因
由于对象的内存空间不存放成员函数,那是否可以存放成员函数的地址?
即使我们可以将类成员函数的地址存放在类对象中,然而实际上我们并不这样做,原因如下:
- 类的成员变量通常是私有的,这意味着每个对象都有自己的成员变量副本,这些变量存储在对象各自的内存空间中。
- 类的成员函数通常是公有的,这意味着所有同一个类的对象共享相同的成员函数实现。如果每个对象都存储成员函数的地址,那么对于每个对象来说,这些地址都将是相同的,这将导致不必要的空间浪费,因为相同的函数地址会被重复存储多次。
- 因此,为了节省空间和提高效率,成员函数的代码是存储在程序的代码段中,这是一个共享的区域,所有同一类的对象都可以访问这里的函数代码。
(2)结论
①类成员函数被声明为公有,意味着它们可以被同一类的所有对象共享。因此,当使用同一类定义不同的对象时,这些对象的成员函数都位于一个共享的公共区域(代码段)进而使得我们不把成员函数存放到对象中。当不同对象调用同一类的成员函数时,它们不是在对象自己的空间中查找该函数,而是访问代码段中的成员函数代码。
②对象的成员变量是私有的,这意味着即使是同一个类的不同对象,它们的成员变量也是不同的。因此,每个对象的成员变量都需要独立存储在各自的内存空间中。相反,对象的成员函数是公有的,这表明同一个类的不同对象调用的成员函数是相同的。因此,这些成员函数的代码只需要在代码段中存储一次,而不需要在每个对象各自的内存空间中又存储成员函数进而造成重复存储并使其浪费内存空间。当不同对象需要调用类中同一功能的成员函数时,它们只需访问存储在代码段(即公共区域)该成员函数的代码即可。
总结:每个对象的成员变量都是独一无二的,因此它们需要在自己的内存空间中独立存储。而对于成员函数,由于同一个类的不同对象调用的是相同的函数代码,所以这些成员函数的代码是存储在共享的公共区域,即代码段中。这意味着,无论有多少个对象,它们调用的同功能成员函数都是指向代码段中的同一份代码。
③当我们用sizeof(对象)函数计算对象的大小时,由于对象中只存放成员变量不存放成员函数而且成员变量是按照内存对齐规则存放在对象中的,所以在用sizeof(对象)函数计算对象的大小实际计算的是遵守内存对齐规则的成员变量大小总和。
3.如何计算类对象的大小
注意:类对象的大小和计算,和C语言一样都要遵守(考虑)内存对齐规则。
3.1.类对象大小的计算公式:sizeof(对象名)
3.2.不同类对象的大小
结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
3.3.解析用仅有成员函数的类、空类来定义对象时对象时对象大小只有1字节的原因
图形解析——结论:用仅有成员函数的类、空类来定义对象时对象中的1个字节是不存储类的数据的,这1个字节只是起到占位作用即利用这1个字节表示对象被实例化定义出来了。(注:判断对象是否被实例化可以从判断对象是否存在地址)
(1) 在C++中,sizeof
操作符用于计算类实例化后的对象大小。对于仅包含成员函数的类或空类,sizeof
通常会返回1。以下是原因和结论的详细解释:
原因:
- 成员函数不占用类对象的存储空间,因为它们是共享的,存储在程序的代码段中,而不是每个对象实例中。
- 空类(即没有成员变量和虚函数的类)本身不占用任何存储空间,因为没有任何数据需要存储。
- 当使用仅包含成员函数的类或空类来定义对象时,
sizeof
返回1字节的原因并不是为了存储类的数据,而是为了确保每个实例都有一个唯一的地址。如果没有这个字节,那么所有空类的实例将具有相同的地址,这在某些情况下可能会导致问题,比如在哈希表中使用这些对象作为键。 - 这1个字节并不存储类的任何数据,它仅仅是起到一个占位符的作用,表明对象已经被实例化并分配了内存空间。
结论:
- 在C++中,即使是一个仅包含成员函数或完全为空的类,当其实例化一个对象时,
sizeof
操作符会返回1字节。这个字节并不是用来存储类数据的,而是为了保证每个对象实例都有一个独一无二的内存地址。这个字节起到了一个标记的作用,表明该位置已经被一个对象实例占用,即使该对象不包含任何实际的数据。这种设计是为了满足内存分配的基本要求,即每个分配的对象都需要有一个唯一的标识。
3.4.结构体内存对齐规则
(1)第一个成员在与结构体偏移量为0的地址处。
(2)其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- VS中默认的对齐数为8
(3)结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
(4)如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
九、this指针
1.this指针的引出
#include<iostream>
using namespace std;
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()
{
//问题1:由于类的成员函数是存储在程序的公共代码段中,当对象d1和对象d2调用相同的函数Init时,
//它们如何区分在函数Init内部访问的是对象d1自己的成员变量_year、_month、_day,而不是对象d2的
//成员变量_year、_month、_day?换句话说,每个对象在调用Init函数时,是如何确保操作的是自己的
//私有成员 变量副本,而不是其他对象的?
Date d1, d2;
d1.Init(2022, 1, 11);
d2.Init(2022, 1, 12);
d1.Print();
d2.Print();
return 0;
}
(1)对于上述类,有这样的一个问题1:在定义了多个同一类的对象之后,由于这些对象的成员函数是公有的并且存储在代码段的同一位置,当不同的对象调用相同的成员函数时,如果该函数内部涉及到对成员变量的操作,那么我们是如何区分并确保每个对象在调用成员函数时,操作的是它自己的那一份成员变量,而不是其他对象的成员变量?
例如:Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
(2)C++中通过引入this指针来解决问题1
解决方式:C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
#include<iostream>
using namespace std;
class Date
{
public:
//虽然我们在定义类时会把成员函数定义成void Init(int year, int month, int day),但是
//这个类Date在编译期间编译器会把类Date中的成员函数void Init(int year, int month, int day)
//的参数加工成void Init(Date* this,int year, int month, int day),把成员函数Init内部调用
//的成员变量加工成this->_year = year、this->_month = month、this->_day = day.
//注意类的成员函数被处理成void Init(Date* this,int year, int month, int day)这个样子不是
//我们自己处理的必须是由编译器自己处理。
//编译器在编译期间对类成员函数加工后的结果:
/*void Init(Data*this,int year, int month, int day)//解析:形参指针Data*this为指向日期类对象
{
this->_year = year;
this->_month = month;
this->_day = day;
}*/
//下面是成员函数的定义(注:这是我们自己定义)
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
//下面都是成员变量的声明
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
//在编译期间编译器会把调用类成员函数的代码改成下面的结果:
//解析:&d1、&d2为类对象的地址
/*d1.Init(&d1,2023, 2, 2);
d2.Init(&d2,2022, 2, 2);*/
//调用类中的成员函数
d1.Init(2023, 2, 2);
d2.Init(2022, 2, 2);
return 0;
}
(3)我们可以通过以下方式观察成员函数内部this指针的情况
注意:this指针的使用只能在成员函数内部使用,无法在成员函数的作用域之外使用。
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
/*_year = year;
_month = month;
_day = day;*/
//我们可以通过以下方式在成员函数内部观察this指针:
//注意:this指针只能在类成员函数的内部使用,而且this指针不能自己手动在实参和形参中添加,
//这个是由编译器自己完成)
cout << this << endl;//注意:指针this是类对象的地址
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
//下面都是成员变量的声明
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2;
d1.Init(2023, 2, 2);
d2.Init(2022, 2, 2);
return 0;
}
(4)在利用class定义类时成员函数有两种定义方式(即成员函数内部访问成员变量有两种方式):一种是显式使用this
指针,另一种是直接引用成员变量。新手可能会倾向于使用this
指针来明确指出成员变量属于当前对象,而经验丰富的程序员通常省略this
指针,因为它在成员函数内部是隐含的。
①方式1:显式使用this
指针
注意:利用class定义类时成员函数可以写成方式1的样子,只不过这种写法是新手写的,在我们了解编译器在编译期间会对类成员函数加工后我们一般是不会写成下面代码。
在C++中定义类时,成员函数内部对成员变量的调用可以写成两种形式。新手可能会采用以下写法,其中显式使用了this
指针:
//新手写类的写法
#include<iostream>
using namespace std;
class Date
{
public:
void Init(int year, int month, int day)
{
//显式使用this指针来指定成员变量属于当前对象
this->_year = year;
this->_month = month;
this->_day = day;
}
private:
//下面都是成员变量的声明
int _year;
int _month;
int _day;
};
②方式2:直接引用成员变量
注意:利用class定义类时成员函数时,成员函数内部是不需要程序员自己手动使用this
指针来引用当前对象的成员变量。编译器在编译过程中会自动将成员函数中对成员变量的引用解释为对this
指针所指向对象的成员变量的引用。所以我们在写类定义时只需写成方式2即可(老手写类的写法)。
然而,一旦我们理解了编译器在编译期间会自动处理this
指针,我们通常会采用更简洁的写法,省略this
指针:
//老手写类的写法
#include<iostream>
using namespace std;
class Date
{
//成员函数公有
public:
//下面是成员函数的定义
void Init(int year, int month, int day)
{
//直接引用成员变量,编译器会自动把_year = year处理为this->_year = year
_year = year;
_month = month;
_day = day;
}
//成员变量私有
private:
//下面都是成员变量的声明
int _year;
int _month;
int _day;
};
③总结:
在这两种写法中,成员函数的实现是相同的;编译器都会在背后处理this
指针。第二种写法(老手写法)更为常见,因为它更简洁,而且当没有局部变量与成员变量同名时,不会引起混淆。
2.this指针的特性
(1)this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
(2)只能在“成员函数”的内部使用
(3)this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针。
(4)this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递。
3.【面试题】
3.1.this指针存在哪里?
①this指针是否存放在对象中?
解答:由于我们在计算对象大小的时候,对象大小只包括遵守内存对齐规则成员变量大小的总和,从对象大小的计算结果可以看出对象大小是不包括this指针,所以我们才说this指针不是存放在对象中。
②this指针存放在下面什么区域:栈、堆、静态区、常量区?
this指针是存放在栈区的。原因:this指针是个隐含形参(注:this指针是编译器在编译期间给对象的成员函数进行传参的),而函数的参数是存放在栈区上的,所以我们才说this指针是存放在栈区上的。
注意:下面展示了,在编译器编译期间对象成员函数在进行传参调用时参数会突然增加1个而且这个参数是指向类对象的指针this,如下图所示:
从下面VS的汇编代码00EC5837 lea ecx,[d1] 可以看出VS的指针this是存放在寄存器ecx中的:
注意:成员函数存放在代码段(公共区域)指的是,成员函数经过编译后编译出来的汇编指令存放在代码段中而不是把成员函数的函数栈帧压入代码段中也不是把成员函数的函数体存入代码段中,而是把成员函数函数体对应的汇编指令存放在代码段中。
总结:对象是存放在栈上的,进而使得对象的成员变量也是存放在栈上的,而对象的成员函数是存放在代码段(公共区域),指针this是存放在栈上而不是存放在对象中而且在VS中指针this是存放在寄存器ecx中。
③在VS中,由于VS的优化,this指针是存放在寄存器ecx中的。
3.2.通过空指针this访问成员变量、成员函数是否一定会发生报错?
注意:对空指针this进行解引用不一定会发生报错。当我们对空指针this进行解引用访问对象的成员变量时由于对象没有定义而且成员变量是存放在对象中的,这才会导致对空指针this进行解引用后就发生程序报错。若类的成员函数内部没有访问成员变量,则当我们对空指针this进行解引用访问对象的成员函数时即使对象没有定义,但是由于成员函数不是存放在对象中的而是存放在代码段(公共区域)中的,进而导致即使对空指针this进行解引用也不会导致程序发生报错。
总的来说,对空指针进行解引用是不一定发生报错的,以访问类成员变量、成员函数为例进行说明。对空指针this进行解引用发不发生报错主要看访问对象是否存放在类实例化后的对象中,由于成员变量存放在类对象中,则对空指针this进行解引用访问成员变量是一定会发生报错;由于成员函数不是存放在类对象中而是存放在代码段中,则对空指针this解引用访问成员函数不会发生报错,而且此时对空指针this解引用是去代码段中查找我们要访问的成员函数。
(1)案例:下面代码是通过空指针this访问成员变量、成员函数
class Date
{
public:
//成员函数的定义
void Init(int year, int month, int day)
{
cout << this << endl;
this->_year = year;
this->_month = month;
this->_day = day;
}
void func()
{
cout << this << endl;
cout << "func()" << endl;
}
//private:
//成员变量的声明
int _year;
int _month;
int _day;
};
//空指针问题
int main()
{
Date* ptr = nullptr;
ptr->func();//正常运行。注意:ptr的作用是进行传参,即ptr存放的使对象地址所以ptr作为实
//参传参给日期类成员函数func()的隐式形参Date*this接收,所以可以认为ptr =
//this。
//ptr->func()正常运行的原因:
//原因1:由于成员函数func()内部没有对类成员变量进行调用,所以ptr->func()才
//正常运行。若成员函数func()内部存在对成员变量的调用,由于成员函数内部一定
//是通过对空指针this解引用访问当前对象的成员变量而且由于成员变量是存放在对
//象中,进而使得对空指针this解引用一定会发生报错。
//原因2:即使ptr是空指针,但ptr->func()中的操作符'->'并不意味着一定是对空
//指针进行解引用的,因为ptr->func()中的操作符'->'的作用不是去对象内存空间中
//查找func()进行调用(因为成员函数不存放在对象中),而是去代码段中查找func()
//进行调用。
(*ptr).func();//正常运行
//注:ptr->func() <=> (*ptr).func()。
//ptr->_year = 0;//运行崩溃->报错原因:由于类成员变量是存放在对象中的,只有定义对象后
//类成员变量才有自己独立空间。由于ptr是个空指针则就是没有定义对象就通
//过空指针ptr访问类成员变量,而且成员变量此时没有自己的空间,进而通过
//针ptr访问类成员变量就是对空指针ptr进行解引用使得程序发生报错。
//总结:
//1.对空指针进行解引用后到底会不会发生报错取决于我们要访问的成员是否
//是必须通过解引用指针ptr中存放的地址,去指针ptr指向的空间中查找我们
//要访问的成员进行调用,若ptr是个空指针而且访问的成员一定存放在ptr指
//向的空间中,则我们通过对空指针ptr进行解引用去指针ptr指向的空间中访
//问成员时一定会发生报错。
//2.对空指针this解引用去访问成员变量、成员函数时到底会不会发生报错取
//决于我们是不是要在指针this指向的对象中查找我们要访问的成员。由于成
//员变量存放在对象中,则通过this解引用去访问成员变量是一定是在对象的
//内存空间中查找我们要访问的成员变量,当this指针是个空指针时,则此时
//对空指针this进行解引用一定会发生报错;
//由于成员函数不是存放在对象中而是存放在代码段中的,则通过this解引用
//去访问成员函数时一定不是在对象的内存空间中查找成员函数而是去代码段
//中找成员函数进行调用,当this指针是个空指针时,则此时对空指针this进
//行解引用一定不会发生报错。
//ptr->Init(2022, 2, 2);//运行崩溃->原因:这里发生报错并不是因为ptr是空指针,这里发生
//报错真正原因实际是Init函数内部存在对类成员变量进行调用而且类
//成员变量是存放在对象内存空间中的由于this是个空指针即对象没有
//定义则此时类成员变量就独立空间,则此时在Init成员函数内部通过
//对空指针this解引用访问类成员变量时一定会发生报错。
//func();//发生报错。报错原因如下所示:
//不用对象而是直接调用类的成员函数会直接发生报错的->不能直接调用的原因:
//原因1.受到类域的限制。即不通过类而是直接调用类的成员函数使得我们无法直接访
//问类域中的成员函数的进而使得程序直接发生报错。
//原因2.由于func函数是定义在类Date中的而我们又没有通过对象或者指向对象的指针
//去访问类Date中的成员函数,而且全局域中也没有func()函数的定义,所以这函数代
//码才会发生报错。
//Date::func();//发生报错->报错原因:由于ptr->func()是通过指针ptr作为实参并传参给成员
//函数func()的形参指针this进而使得我们才能调用日期Date的成员函数,所以
//Date::func()之所以发生报错是因为没有给成员函数传实,没有传参就无法调
//用类的成员函数。
return 0;
}
(2)图形解析:
- ptr是个空指针。从下面汇编指令可以看出,当我们通过指针来调用对象的成员函数时,无论是使用箭头操作符
->
(ptr->func()
)还是点操作符.
通过解引用指针((*ptr).func()
),底层实现上是相同的。这两种方式都会在编译时生成相似的汇编代码,用于调用成员函数。 - 在调用成员函数之前,指针
ptr
会被用作隐式的参数传递给成员函数隐式形参this
指针而且此时this指针是个空指针。 - 从汇编指令中我们可以观察到,对于
ptr->func()
和(*ptr).func()
的调用,并没有直接包含对空指针ptr
进行解引用的汇编代码。这表明,在底层实现上,无论是使用箭头操作符还是通过解引用点操作符来调用成员函数,都不会直接对ptr
进行解引用操作。这是因为成员函数的代码位于代码段中,而不是对象内部。当我们调用成员函数时,实际上是跳转到代码段中对应的函数地址,而不是从对象内存中取出函数代码。因此,ptr->
和*ptr
的使用并不涉及对空指针ptr
的解引用,这也是为什么在汇编指令中没有出现相关的解引用操作。 - 下面汇编指令00D861C1 mov ecx,dword ptr [ptr]的意思是把指针ptr的值即对象地址存放在寄存器ecx中,然后再把寄存器ecx中的对象地址传参给成员函数的隐式形参this指针接收。
3.3.this指针可以为空吗?
知识点
在C++中,类的成员变量和成员函数的存放位置决定对空指针this解引用来访问成员变量、成员函数是否一定会发生报错有直接的关系。以下是详细的分析:
成员变量和成员函数的存放位置:
-
成员变量:类成员变量是存放在对象的内存空间中。当创建完一个对象后类成员变量才算有自己独立空间即类成员变量才会被分配在栈上(对于自动存储期的对象)或者堆上(对于动态分配的对象)。
-
成员函数:成员函数的代码是存放在代码段中的,这是程序的一个只读部分,它包含了程序的可执行指令(即成员函数函数体对应的汇编指令)。无论有多少个对象实例,成员函数的代码只有一份,并且所有对象共享这份代码。
对空指针this解引用:
-
成员变量:当你通过一个空指针访问成员变量时,因为空指针不指向任何有效的对象,所以这个指针没有指向有效的内存位置来存储成员变量。尝试解引用空指针来访问成员变量会导致未定义行为,通常会导致程序崩溃。
-
成员函数:成员函数的地址确实是固定的,并且独立于对象实例。在C++中,成员函数在内部会接收到一个隐式参数指针this且this指针指向调用该函数的对象,即使函数看起来没有直接使用
this
,编译器生成的代码仍然会使用它。当我们通过空指针来调用成员函数会使得this指针是个空指针,只要成员函数内部不使用this
指针,或者不尝试访问任何成员变量,函数调用本身通常可以正常运行;当成员函数的隐式参数this指针是个空指针时,若我们使用了this
指针来访问类的成员变量的时候则会造成对空指针(即this指针)进行解引用进而导致程序崩溃。
总结:
-
解引用空指针this访问成员变量:这是非常危险的,因为空指针没有指向任何有效的对象,所以无法访问成员变量。这几乎总是会导致程序崩溃。
-
通过空指针this调用成员函数:如果成员函数内部不使用
this
指针访问任何成员变量,则函数调用本身通常可以正常运行。如果成员函数内部使用了this
指针访问任何成员变量,那么会造成对空指针this指针进行解引用进而导致程序崩溃。
总之,无论是访问成员变量还是调用成员函数,都应该确保指针不是空指针,以避免未定义行为和程序崩溃。
十、对比C语言和C++关于封装的问题
C语言和C++实现栈的差异
1.C++与C语言实现栈的各自思路
(1)C语言实现一个栈的思路
(2)C++实现一个栈的思路
把栈的成员变量(即指向动态数组的指针a、统计栈存放数据个数size、栈最大容量capacity)、成员函数(即栈的各个功能函数:压栈、出栈、取栈顶元数等等)封装在类中。
2.C语言和C++实现栈的差异
2.1.从语法结构分析C语言和C++实现栈的差异
(1)C语言和C++在实现栈的逻辑概念上是相同的,但它们在语法实现上存在差异。数据结构的逻辑实现与编程语言无关,差异主要体现在语法细节上。
(2)C语言实现栈时采用面向过程的方法:首先定义一个结构体来存放栈的数据,然后定义一系列独立的函数来操作这个栈。这种做法将数据和操作数据的方法分离开来。
(3)C++实现栈时采用面向对象的方法。在C++中,我们将栈的数据(成员变量)和操作数据的方法(成员函数)封装在一个类中,这样可以更好地管理和控制数据的访问。
(4)C语言和C++实现栈的真正(本质)差异是:
①C语言中的数据访问较为自由,没有严格的限制,可能会导致不安全的访问模式。下面C语言关于取栈顶元素的两种方式可以说明C语言的数据是可以随意访问的:
解析:
- 在C语言中,访问栈顶元素可以通过多种方式实现。例如,一种方式是通过封装好的函数来安全地获取栈顶元素,如
int top = StackTop(&st);
。这种方式是安全的,因为它通常会检查栈是否为空以及栈顶元素是否可访问。 - 然而,另一种方式
int top = st.a[st.top - 1];
直接访问数组元素,这种方式是不安全的。尽管C语言允许这种直接的数据访问,但这种做法存在风险,因为它绕过了任何可能的安全检查。如果st.top
的值不正确(比如它可能小于1),那么这种访问方式可能会导致数组越界,进而引发未定义行为,包括程序崩溃。确实,C语言支持这种不安全的访问方式,因为它将数据和操作数据的函数分离,不强制执行封装,因此程序员可以直接访问结构体的成员。但是,这种访问方式需要程序员对栈的实现细节有深入的了解,包括栈的初始化和栈顶指针的管理,最终才能安全地进行访问,但是这样就会显得很麻烦。 - 总结:在C语言中,访问栈顶元素可以通过封装好的函数进行,这是一种安全的做法,因为它通常会包含必要的边界检查。然而,C语言也允许程序员直接通过数组索引来访问栈顶元素,如
int top = st.a[st.top - 1];
。这种直接访问方式是不安全的,因为它可能绕过栈的状态检查,如果使用不当,可能会导致数组越界或其他运行时错误。由于C语言的数据访问较为自由,程序员需要谨慎操作,并确保了解栈的底层实现细节,以避免不安全的访问。
②而C++通过类的封装,限制了直接访问数据(即成员变量),必须通过成员函数(即方法)来安全地访问和操作数据(即成员变量)。
(2)从底层结构分析C语言和C++实现栈的差异
①大小:C语言定义的结构体栈st和C++定义的对象st是不是一样大?一样大。
在底层结构上,C语言定义的栈结构体和C++定义的栈对象在大小上是相同的,因为C++对象的大小只包括成员变量的大小,不包括成员函数的大小。而C语言定义的栈结构体成员变量。
② 功能函数的命名: 在C++中,成员函数的命名不需要加前缀,因为它们是类的一部分,类的名称通常反映了数据结构的名称,所以成员函数的名称可以直接是Pop、Push、Top等。而在C语言中,由于函数是独立于数据结构的,为了避免命名冲突,通常会给函数名加上前缀,如StackPop、StackPush、StackTop,以明确指出这些函数操作的是栈数据结构。
③功能函数的调用并传参: C++在调用成员函数时提供了更便捷的方式。在C++中,不需要显式传递对象的地址给成员函数,因为编译器会自动将对象的地址(即this指针)传递给成员函数。例如,调用st.Pop()、st.Top()、st.Push()时,编译器会内部将这些调用转换为类似于st.Pop(&st)、st.Top(&st)、st.Push(&st)的形式。而在C语言中,必须显式传递结构体的地址给每个功能函数,如StackPop(&st)、StackPush(&st)、StackTop(&st),这是因为C语言的功能函数不是类的成员,它们需要知道操作的具体数据结构地址。
总的来说,C++在传参方面比C语言要传少1个参数。
④底层函数的实现基本原理是相似的:C++的成员函数和C语言的功能函数在底层实现上遵循相似的原理,但是它们在细节上有所不同。在C语言中,由于没有类的概念,功能函数需要显式地传递数据结构的地址,以便函数能够访问和操作该数据结构。例如,对于栈的实现,C语言中的功能函数可能会定义为 void StackPush(Stack* stack, int value)
,其中 Stack* stack
是指向栈结构的指针。
在C++中,成员函数属于类的定义,并且在调用成员函数时,编译器会隐式地传递对象的地址(即this指针)给成员函数。这样,成员函数就可以在内部通过this指针访问对象的成员变量。例如,C++中的成员函数可以定义为 void Push(int value)
,当调用 stack.Push(value)
时,编译器实际上会将调用转换为类似于 Stack::Push(&stack, value)
的形式,其中 &stack
是对象的地址。
因此,虽然在底层实现上,C++的成员函数和C语言的功能函数都需要一个指向数据结构的指针来访问和操作数据,但C++通过this指针机制提供了更加便捷和面向对象的调用方式。
⑤在底层结构上,C语言和C++实现栈的方式存在差异。C语言使用结构体和函数来分别表示栈的数据结构和操作,而C++使用类来封装栈的数据和操作。在内存布局上,C++对象的成员变量与C语言的结构体成员相似,但C++的成员函数在对象内存中并不占用空间,它们是类的一部分,由编译器在调用时通过this指针隐式传递对象地址。
3.用C语言、C++实现栈的详细代码
3.1.C语言实现栈
//.c
#include<assert.h>
#include<stdlib.h>
#include<stdio.h>
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
void CheckCapacity(Stack* ps)
{
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->array, newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
ps->array = temp;
ps->capacity = newcapacity;
}
}
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps);
ps->array[ps->size] = data;
ps->size++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->size;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->size--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->array[ps->size - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackDestroy(&s);
return 0;
}
可以看到,在用C语言实现时,Stack相关操作函数有以下共性:
- 每个函数的第一个参数都是Stack*
- 函数中必须要对第一个参数检测,因为该参数可能会为NULL
- 函数中都是通过Stack*参数操作栈的
- 调用时必须传递Stack结构体变量的地址
结构体中只能定义存放数据的结构,操作数据的方法不能放在结构体中,即数据和操作数据
的方式是分离开的,而且实现上相当复杂一点,涉及到大量指针操作,稍不注意可能就会出
错。
3.2.C++实现栈
//.cpp
#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top()
{
return _array[_size - 1];
}
int Empty()
{
return 0 == _size;
}
int Size()
{
return _size;
}
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}
C++中通过类可以将数据 以及 操作数据的方法进行完美结合,通过访问权限可以控制那些方法在
类外可以被调用,即封装,在使用时就像使用自己的成员一样,更符合人类对一件事物的认知。
而且每个方法不需要传递Stack*的参数了,编译器编译之后该参数会自动还原,即C++中 Stack *
参数是编译器维护的,C语言中需用用户自己维护。