目录
1、类的引用
1.1 类的成员函数
1.2 类成员函数的声明和定义
2、类的定义
2.1 类的访问限定(封装)
3、类重名问题
4、类的实例化
4.1 类的大小
5、隐含的this指针
5.1 空指针问题
结语:
前言:
C++的类跟c语言中的结构体在概念上是一样的,只不过在c语言中叫结构体,而在C++中叫类。c语言中的结构体内只能定义变量,而C++在此基础上升级后,类里面可以定义变量和函数,并且把类中的内容叫做类的成员,类中的变量叫做成员变量,类中的函数叫做成员函数。
1、类的引用
在c语言中,结构体的类型是:struct+结构体名称,在没有使用typedef对其进行重命名时,是不能省略struct的。例子如下:
struct ListNode//定义一个结构体
{
struct ListNode* Node;
int data;
};
int main()
{
struct ListNode lst;//定义一个结构体变量时,不能省略的掉struct
return 0;
}
然而在C++中,就可以做到不使用typedef的情况下,省略struct并使用该结构体类型(这里暂且把类叫做结构体,因为用的还是struct定义出来的,方便理解)。例子如下:
struct ListNode//定义一个结构体,也可以理解成定义一个类
{
ListNode* Node;//成员变量的类型也可以不加struct
int data;
};
int main()
{
ListNode lst;//定义结构体变量时,可以不加struct
return 0;
}
1.1 类的成员函数
用c语言实现的栈或者链表,通常是把各个功能函数放在结构体的外面,用的是让函数与结构体成员分开的写法。而在C++中,可以把这些功能函数都放到结构体(类)中,让函数与结构体成员都处于同一作用域。
类中函数的写法:
#include<iostream>
using namespace std;
struct Stack//结构体(类)
{
//成员函数
//栈的初始化
void Init(int n=4)
{
arr = (int*)malloc(sizeof(int) * n);
if (arr == nullptr)
{
perror("malloc");
return;
}
Top = 0;
capacity = n;
}
//压栈
void push(int x)
{
if (capacity == Top)
{
int newcapacity = 2 * capacity;
int* temp = (int*)realloc(arr, sizeof(int*) * newcapacity);
if (temp == nullptr)
{
perror("Push");
return;
}
arr = temp;
capacity = newcapacity;
}
arr[Top++] = x;
}
//成员变量
int* arr;
int Top;
int capacity;
};
int main()
{
Stack st1;//创建结构体变量
st1.Init();//调用函数时要表明调用对象
st1.push(1);
st1.push(102);
cout << st1.arr[st1.Top - 2] << endl;//打印栈里元素
cout << st1.arr[st1.Top - 1] << endl;
return 0;
}
运行结果:
可以看到上述代码中,栈的初始化和压栈函数都是直接写在结构体作用域中,而且能够正常实现栈的功能,说明C++支持把函数写进结构体内。并且注意调用函数时要表明调用对象,因为此时在类中的函数不再是全局范围的了,而是只属于当前类,因此调用成员函数时要先创建一个变量,并且用改变量去调用(写法和调用结构体成员一样)。
1.2 类成员函数的声明和定义
我们一般实现某个功能函数时,都是把该函数的声明放在头文件内,把该函数的定义放在.cpp文件中,做到声明和定义分开,那么在C++中如何实现函数的声明和定义分离呢。
比如把上述代码分成三个文件:3.cpp、3.h、test.cpp。3.cpp用于存放成员函数的定义,3.h是结构体的创建,test.cpp是主函数实现。
3.h代码如下:
#pragma once
#include<iostream>
using namespace std;
struct Stack
{
//成员函数声明
void Init(int n = 4);//栈的初始化
void push(int x);//压栈
//成员变量
int* arr;
int Top;
int capacity;
};
3.cpp代码如下:
#include"3.h"
//成员函数定义
void Stack::Init(int n )//栈的初始化,注意添加作用域限定符
{
arr = (int*)malloc(sizeof(int) * n);
if (arr == nullptr)
{
perror("malloc");
return;
}
Top = 0;
capacity = n;
}
void Stack::push(int x)//压栈,注意添加作用域限定符
{
if (capacity == Top)
{
int newcapacity = 2 * capacity;
int* temp = (int*)realloc(arr, sizeof(int*) * newcapacity);
if (temp == nullptr)
{
perror("Push");
return;
}
arr = temp;
capacity = newcapacity;
}
arr[Top++] = x;
}
test.cpp代码如下:
#include"3.h"
int main()
{
Stack st1;//创建结构体变量
st1.Init();
st1.push(1);
st1.push(102);
cout << st1.arr[st1.Top - 2] << endl;//检查压栈是否成功
cout << st1.arr[st1.Top - 1] << endl;
return 0;
}
可以从3.cpp文件中看到,声明成员函数的定义写法跟以前直接定义函数不一样,而是在成员函数名的前面加了作用域限定符’::’,表达的是该函数并不是全局的函数,而是只针对Stack结构体类型的函数。
声明和定义分离后的运行结果:
从结果可以看到,即使分成三个文件,只要使用作用域限制符依然是可以正常运行的。
2、类的定义
C++的类其实就是c语言中的结构体,只是类相比于结构体是做了升级的。因为C++兼容c语言,因此仍然可以采用struct来定义一个类,而且是支持类的相关功能的。但是在C++中基本都是用关键字class来定义一个类,比如创建一个栈的类,具体写法为:
class Stack//class后面跟类的名称
{
//括号内存放成员变量和成员函数
};
int main()
{
Stack st1;//st1在c语言中是变量,但是在C++中,更喜欢把st1叫做对象
return 0;
}
2.1 类的访问限定(封装)
既然了解了class的作用后,将上述代码的struct替换成class,真正的去使用C++的类,但是发现替换后编译器开始报错了:
报错显示类中的成员都不可访问, 主要是因为C++的类相比于c语言的struct更加的安全,具体体现在类中的空间分为私有域和公有域,公有域是可以让类外随意访问的,而私有域拒绝让类外访问。在用class创建类时,如果没有明确对类进行公有域和私有域的划分,那么默认类里的所有域为私有域,这也是报错的原因。
访问限定符说明:
1、public修饰的成员是可以让类外进行访问。
2、protected和private修饰的成员不可让类外直接进行访问。
3、一个访问限定符的作用域范围是直到遇到下一个访问限定符或者遇到‘}’。
4、class的默认访问权限是private(这也是上述代码报错的原因),而struct默认访问权限是public(这也是为什么上述代码用struct就能够正常运行)。
像上述把类分成两个区域的这一操作又称为封装,封装:隐藏对象的细节,对外只公开接口,让外部通过接口与对象达成交互。目的是为了更安全的使用代码,通常是把成员变量都放在私有域中,而成员函数放在公有域中,提高用户使用代码的安全性。
把struct替换成class并且优化后的代码如下:
#include<iostream>
using namespace std;
class Stack//结构体(类)
{
public:
//成员函数
//栈的初始化
void Init(int n = 4)
{
arr = (int*)malloc(sizeof(int) * n);
if (arr == nullptr)
{
perror("malloc");
return;
}
Top = 0;
capacity = n;
}
//压栈
void push(int x)
{
if (capacity == Top)
{
int newcapacity = 2 * capacity;
int* temp = (int*)realloc(arr, sizeof(int*) * newcapacity);
if (temp == nullptr)
{
perror("Push");
return;
}
arr = temp;
capacity = newcapacity;
}
arr[Top++] = x;
}
void Print()//只能在类中进行对private的访问
{
cout << arr[Top-1] << endl;
}
private:
//成员变量
int* arr;
int Top;
int capacity;
};
int main()
{
Stack st1;//创建结构体变量
st1.Init();
st1.push(1);
st1.push(102);
st1.Print();
return 0;
}
因此如果要访问类的私有域内容,只能在通过类中的函数进行访问。
3、类重名问题
例如,现对一个日期类进行初始化:
#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()
{
Date dt1;//创建对象
dt1.Init(2022, 2, 2);
dt1.Print();
return 0;
}
运行结果:
可以发现结果竟然是随机值,原因就是初始化函数的形参与类成员变量同名,然后编译器遵循局部优先的概念,把左值当成了形参本身,结果是形参自己给自己赋值,类成员变量并没有完成初始化。
解决方法:把成员变量名和形参名进行区分即可。
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;
};
4、类的实例化
用类的类型去创建一个对象,该操作称之为类的实例化,跟c语言中创建变量是一个意思。在对类实例化之前是不能直接去访问类中的内容,因为类好比一个构想图,光有一份构想图是无法真正获得图内的东西,只有在类实例化后,即真正实现了构想图的内容才可以实际的获得其中。因此在实例化后,系统会为该对象开辟一块空间,这时候以该空间为对象,可以去访问他的类。
类的实例化代码例子如下:
class Date
{
//....
public:
int _year;
int _month;
int _day;
};
int main()
{
Date._year = 12;//错误写法,不能直接访问类
Date dt1;//类的实例化
Date dt2;//类的实例化
//实例化后,可以通过对象访问该对象的类
dt1._day = 12;
dt2._day = 12;
return 0;
}
4.1 类的大小
一个结构体的大小是根据他的成员类型计算出来的,但是一个类所包含的不只有成员变量,还有成员函数,那么一个类的大小该如何计算呢,用上述日期类作为例子,计算该类的大小。
可以看到,尽管调用了成员函数,但是该类的大小依然是12,说明系统只为该类的成员变量开辟了空间,并没有给成员函数调用空间。
因为不同对象的成员变量肯定是不一样的,就拿日期类举例,对象dt1的成员变量可以是2022.2.2,但是重新实例化一个对象dt2的成员变量可以为2023.3.3。只是对象dt1和dt2再调用其成员函数时,实质上全部调用的都是同一个成员函数,因为函数都是实现相同功能,比如dt1中的初始化函数和dt2中的初始化函数所实现的功能都是一样的,因此把类的成员函数放在公共区域(即代码段中),不同对象再调用函数时,统一去代码段中调用,因此函数不计入类的大小。
5、隐含的this指针
上述说到了类成员函数是放在公共区域的,那么问题来了,不同的对象再调用同一个函数时,编译器是如何知道是哪个对象调用的,因为传参的时候传的只有实参数据,并无其他区分对象的标记号。
当对象调用函数时,编译器会自行给函数的实参和形参补上该对象的地址和指针,如下图:
编译器会自动把对象的地址一并当作实参传递给函数形参,并且会添加一个(隐藏指针)this指针作为形参接收对象的地址,用指针this就能访问并且修改具体对象里的成员变量了 ,当然这都是编译器自动完成的。我们可以在函数内部使用this指针,但是不能在形参和实参上直接手动添加。因为是形参,因此this指针存放在栈空间,在vs环境下,this指针通过ecx寄存器进行自动传递,所以无需用户干涉过程。
5.1 空指针问题
我们都知道如果对一个空指针进行解引用是会报错的,那么以下代码的运行结果是什么呢?
#include<iostream>
using namespace std;
class example_one
{
public:
void Print()
{
cout << "Print()" << endl;
}
void Init(int x)
{
_a = x;
}
private:
int _a;
};
int main()
{
example_one* p = nullptr;//定义一个指针p指向空
p->Print();
return 0;
}
运行结果:
结果竟然是正常运行并且打印了Print函数内的信息, 当我们看到语句:p->Print(),第一反应都是对p进行解引用,对空指针进行解引用肯定会报错,原因在于这里并没有对p进行解引用,只是调用了类型为example_one的对象里的函数Print(因为函数不是存储在类里面,而是存储在代码段中),并且把指针的内容传给函数Print,这一过程并没有对空指针进行解引用。
然而下面这种写法就会报错:
#include<iostream>
using namespace std;
class example_one
{
public:
void Print()
{
cout << "Print()" << endl;
}
void Init(int x)
{
cout <<this<< endl;//打印出来的是00000000,表示空指针
_a = x;//这里可以理解为*this->_a,对空指针this解引用,因此报错
}
private:
int _a;
};
int main()
{
example_one* p = nullptr;
//p->Print();//可以正常运行
p->Init(1);
return 0;
}
结合this指针的概念,这里把p的值作为实参传递给了this指针,因此发生错误的原因在于Init函数内部对this指针进行了解引用操作,即对空指针进行解引用操作,导致报错。
所以‘->'并不一定是解引用操作,关键点在于右值是否为类里的成员变量,如果只是成员函数那么‘->'不为解引用操作,如果右值为成员变量则‘->'表示解引用操作。
结语:
以上就是关于C++_类的讲解,类与结构体在概念上虽然相似,但是类的细节更多,较结构体更复杂,类作为C++中的基础需掌握好。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充~!!谢谢大家!!( ̄︶ ̄)↗