前言
C语言主要是一种面向过程的语言,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
但在处理复杂问题时,代码可能会变得冗长和难以维护,模块之间相互依赖的程度高不利于代码的扩展和复用。
因此,C++在其基础上增加了面向对象的特性。面向对象将一件事情拆分成不同对象,通过对象的交互解决问题。(同时C++也兼容了C语言的面向过程特性)
一、类的引入
在C语言中,结构体内只能定义变量。在C++中,结构体内不仅可以定义变量,还可以定义函数。(C++兼容C语言)
我们拿数据结构的栈举例
(1)C语言实现的栈
//C语言的struct的所以成员变量默认是公有的
struct Stack
{
// 成员变量
int* a;
int top;
int capacity;
};
//函数放在外部
void StackInit(Stack st)
{
// ...
}
void StackPush(Stack st,int x)
{
// ...
}
(2)C++实现的栈
//C++的类中所有成员变量、函数默认是私有的
struct Stack
{
// 成员函数
void Stack()
{
// ...
}
void StackPush(int x)
{
// ...
}
// 成员变量
int* a;
int top;
int capacity;
};
通过观察C++和C语言实现栈的方式,可以发现C++允许函数在结构体内部声明,C语言的struct升级成了C++的类。在C++中, struct
可以用 class
代替 。
此外,C++的结构体在定义变量上和C语言的结构体有所区别
在C语言中声明结构体变量时需要加上关键字 struct
struct Person {
char name[50];
int age;
};
struct Person person1;
在C++中,声明结构体变量可以省略关键字 struct
,直接用结构体名作为类型名。(C++编译器默认将struct、class和union的类型名视为在其作用域内可见,而不需要显式的前缀)
struct Person {
char name[50];
int age;
};
//使用结构体名Person作为类型名
Person person1;
Q:那么在C++中 struct 和 class 有什么区别呢?
(1)成员访问权限
- struct 中的成员默认是public(公有)的,可以在类外部访问。
- class 中的成员默认是private(私有)的,除非被声明为 public 或者 protected 否则在类外部无法访问。
(2)默认成员访问标签
- 在C语言中,结构体无法使用访问标签指定成员的访问权限。但是在C++中,结构体和类均有访问权限的设定,private、protected、public三种。
(3)默认继承访问权限
- struct 的默认继承访问是public 的
- class 的默认继承访问权限是private的
在C++中,struct 和 class 均可以 继承 struct 或 class。默认的继承访问权限是取决于子类,而不是取决于基类。(子类可以继承基类的属性和方法,也可以添加新的属性和方法,或者覆盖(Override)基类中已有的方法)
二、类的定义
⭐️ 基本结构
由class关键字,类名,类的主体三部分构成
class ClassName
{
// 类体:由成员函数和成员变量组成
};//分号结尾
class是定义类的关键字,ClassName是类名,大括号中为类的主体,注意类后的分号不能省略。
类的主体由两部分构成:
(1)成员变量(类的属性)
(2)成员函数(类的方法)
⚡️ 定义方式
(1)成员函数的声明和定义全部放在类体中
#include<iostream>
//class没设置访问标签时默认为私有
class Stack
{
//成员变量
int* _array;
int _top;
int _capacity;
//成员函数
public :
void Init(int capacity=4)
{
_array = (int*)malloc(sizeof(int) * capacity);
_capacity = 0;//初始化为0,如需扩容需要手动赋值
_top = 0;//top指向栈顶元素的下一个
}
void Push(int x)
{
if (_top == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
int* tmp = (int*)realloc(_array, newcapacity * sizeof(int));
//STDateType* tmp = ps;
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_array = tmp;
_capacity = newcapacity;
}
//top始终指向栈顶元素
//动态内存分配:包含 N 个 STDateType 类型元素的内存块,可以像数组一样使用。
_array[_top] = x;
_top++;
}
};
int main()
{
Stack st1;
st1.Init();
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
}
注意:当成员函数在类定义内部被直接定义时(没有使用inline关键字),编译器可能会选择将该函数视为内联函数。
对于类成员函数,尤其是那些体积小、调用频繁的成员函数,内联展开可以显著提高代码的执行效率。编译器会基于函数的大小、是否含有复杂的控制结构(如循环、递归)、是否访问全局变量等多个因素来决定是否将成员函数内联化。
(2)成员函数的声明和定义分离
声明放在头文件(.h文件)
test.h
#pragma once
#include<iostream>
using namespace std;
class Stack
{
//设置为公有,否则无法类外部访问
public:
//成员函数
void Init(int capacity = 4);
void Push(int x);
private:
//成员变量
int* _array;
int _top;
int _capacity;
};
定义放在源文件(.cpp文件)
test.cpp
#include "test.h"
void Stack::Init(int capacity)
{
_array = (int*)malloc(sizeof(int) * capacity);
_capacity = 0;//初始化为0,如需扩容需要手动赋值
_top = 0;//top指向栈顶元素的下一个
}
void Stack::Push(int x)
{
if (_top == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
int* tmp = (int*)realloc(_array, newcapacity * sizeof(int));
//STDateType* tmp = ps;
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_array = tmp;
_capacity = newcapacity;
}
//top始终指向栈顶元素
//动态内存分配:包含 N 个 STDateType 类型元素的内存块,可以像数组一样使用。
_array[_top] = x;
_top++;
}
int main()
{
Stack st1;
st1.Init();
st1.Push(1);
st1.Push(2);
st1.Push(3);
return 0;
}
注意
- 当我们在头文件声明了一个类,就创建了一个类作用域。在其他文件定义类的方法时,需要加上作用域限定符
::
- 成员变量在命名时,建议在前面加上一个下横线_,这样可以方便与形参进行区分
class Person
{
//对成员变量做一些修饰,方便与形参区分
int _name;
int _age;
public:
void Init(int name,int age)
{
_name=name;
_age=age
}
};
三、类的访问限定符
访问限定符
在C++中,类的成员(成员变量、成员函数)可以有不同的访问权限。访问权限的设置由访问限定符来实现。访问限定符的主要目的是封装对象的内部实现,保护对象的状态不被外部随意修改,从而提高代码的健壮性和可维护性。
C++有三种访问限定符:public(公有)、private(私有)、protected(受保护)
(1)public(公有)
公有成员在类的外部是可访问的。任何外部代码(包括其他类的对象)都可以访问公有成员。公有成员通常用于定义类的接口,即类如何与外部世界交互。
如上面举例的栈,用访问限定符将成员函数设定为公有后,可以在类外部直接使用该成员函数
(2)private(私有)
私有成员在类的外部和派生类中都不可直接访问。它们只能被类的成员函数(包括友元函数)和友元类访问。私有成员通常用于封装类的内部实现细节,防止外部代码直接访问这些细节。
(3)protected(受保护)
受保护成员在类的外部是不可直接访问的,但可以在派生类(即子类)中被访问。这允许派生类继承并访问基类的某些成员,同时保持这些成员对外部世界的隐藏。
注意
- 如果没有手动设置访问限定符,class默认是私有的,struct默认是公有的(struct要兼容C)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
访问限定符的作用主要发生在编译时,而不是运行时。编译器会根据访问限定符来检查代码中的访问是否合法。如果尝试访问一个不应该被访问的成员(例如,从类的外部访问一个private成员),编译器会报错。
四、类的封装
(1) 概念
它指的是将对象的属性和方法封装在一起,并限制外部访问这些属性和方法的方式。封装可以被认为是一个保护屏障,用于防止类的代码和数据被外部随意访问,从而提高代码的安全性、可维护性和可重用性。
将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
(2)使用场景
在C++中,使用类将数据和方法结合,通过调整访问权限来隐藏对象的内部细节,开放成员函数的权限以供使用,就是一种封装
五、类的实例化
类的实例化即使用类作为模板来创建对象的过程。类就像建筑师的一张图纸,可以根据图纸建成许多具有相同骨架的房子。
注意
(1)类的声明限定了类的成员函数和成员变量,但是没用分配实际内存空间来存储,只有定义了基于该类的新对象,才会分配实际内存空间
(2)一个类可以实例化出多个对象。实例化出的对象占用实际的物理空间,存储类的成员变量。
六、类对象模型
🍿 类对象的大小及存储方式
class Stack
{
public:
void Init(int capacity = 4);
void Push(int x);
private:
int* _array;
int _top;
int _capacity;
};
我们创建对象st1和st2,在vs x64环境下 使用sizeof()打印对象的大小
可以看到,基于Stack类定义的对象大小为12。由此可以推导出:类对象的大小只计算类的成员变量,不计算成员函数。
那么问题来了,当类中没有成员变量,只有成员函数呢?二者都没有呢?
//只有成员变量
class A
{
int a;
};
//只有成员函数
class B
{
void Init()
{
}
};
//没有成员函数也没有成员变量
class C
{
};
在C++中,当类中只有成员函数或是类为空类实例化对象时,会开辟一个byte的空间进行占位,不然无法区别同一个类实例化出来的两个对象。
C++需要确保每个对象在内存中都有一个唯一的地址。如果没有为对象分配任何内存空间,那么两个或多个空类对象可能会共享相同的内存地址,这在逻辑上是不允许的。因此,即使是一个空类,编译器也会为其分配至少一个字节的内存,以确保每个对象都有一个唯一的内存地址。
类对象的存储方式:只保存成员变量,成员函数存放在公共代码段。
🍯 结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。(vs中默认对齐数为8)
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整
体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
-
为什么要内存对齐,如何内存对齐?
可以参考这篇博客——内存对齐 -
可以让结构体按照自己设置对齐参数进行对齐吗,如何设置?
可以,使用
#pragma pack(n)
设置默认对齐数为n
- 什么是大端小端,如何测试机器是否是大端小端?
大端:数据的高位字节存储在低地址中,低位字节存储在高地址中。
小端:数据的低位字节存储在低地址中,高位字节存储在高地址中。
int check_sys()
{
int a = 1;
return *(char*)&a;
//访问a的首字节(在内存中的最低地址)
}
int main()
{
if (check_sys()== 1)
printf("小端\n");
else
printf("大端\n"); return 0;
}
七、this指针
🍧 引入
通过上面的学习,我们了解到类的成员变量存储在类中,成员函数存储在代码段。那么当我们从外部调用类的成员函数时,成员函数是如何区分是哪个对象调用的呢?
以下面代码举例,我们定义了一个类
#include<iostream>
using namespace std;
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()
{
date d1;
date d2;
d1.Init(2024, 9, 22);
d2.Init(2004, 10, 4);
return 0;
}
可以看到 date类中有Init()公有成员函数,当我们创建好对象d1、d2,再通过d1或d2去调用date类的Init()函数时,函数是怎么知道是哪个对象调用的呢?
C++中引入了this
指针
this指针指向调用函数的对象,并隐式传递给函数。即所有成员变量都是通过this指针去访问的。
需要注意的是,this指针的操作对用户是透明的,我们不能去手动传递this指针,编译器会自动传递。用户直接使用即可。
🍨 用途
(1)访问成员变量
当成员变量与成员函数的参数或局部变量同名时,可以使用this->成员变量名来明确指定要访问的是成员变量。
#include<iostream>
using namespace std;
class date
{
public:
//成员变量和成员函数参数同名,用this来区分
void Init(int year,int month,int day)
{
this->year = year;
this->month = month;
this->day = day;
}
private:
int year;
int month;
int day;
};
int main()
{
date d1;
date d2;
d1.Init(2024, 9, 22);
d2.Init(2004, 11, 4);
return 0;
}
(2)返回当前对象(链式操作)
在成员函数中,可以返回this(对象的引用)来支持链式操作。例如,在设置类成员变量后返回对象本身的引用,从而允许连续调用多个成员函数。
date& Init(int year,int month,int day)
{
this->year = year;
this->month = month;
this->day = day;
return *this;
}
🍦 特性
- this指针的类型:this指针的类型是指向类类型的指针,即
ClassName* const
。这意味着this指针是常量指针,其值(即指向对象的地址)不能被修改,但可以通过this指针访问和修改对象的内容。 - this指针只能在类中的非静态成员函数中使用,不能再类中的静态成员函数使用,也不能在类外使用。
静态成员函数没有this指针,因为它们不与任何对象实例关联。静态成员函数可以访问类的静态成员,但不能访问非静态成员
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
this指针通常存储在函数的栈帧中。当成员函数被调用时,栈帧为该函数分配内存,并在其中包含this指针。
但在vs下一般会用ecx寄存器直接传递。(this指针会因编译器不同而存储在不同的位置)
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
int main()
{
date d1;
d1.init(2024, 9, 21);
// 编译器自动转换
// d1.init(&d1 , 2023, 6, 6);
// 注意,传参时用户不能将&d1作为参数传给函数
return 0;
}
Q:this指针可以为空吗,什么情况可以正常运行,什么情况无法编译或程序崩溃呢?
#include<iostream>
using namespace std;
class date
{
public:
void Init(int year,int month,int day)
{
this->year = year;
this->month = month;
this->day = day;
}
void greet()
{
cout << "你好哇" << endl;
}
void introduce()
{
cout << "日期是:" << this->year << "年" << this->month << "月" << this->day << "日" << endl;
}
private:
int year;
int month;
int day;
};
int main()
{
date *d1=nullptr;
//可以通过编译,正常运行
d1->greet();
//成员函数introduce需要打印对象内的成员变量,需要this指针解引用,但是d1是空指针,导致程序崩溃
d1->introduce();
//给对象的成员变量赋值,需要this指针解引用,但是d1是空指针,导致程序崩溃
d1->Init(2024, 9, 22);
return 0;
}
从测试结果来看,当this为空指针,只要this不解引用,程序就可以正常运行。
至于如何判断this是否解引用,关键在于访问的成员函数内部是否有对象中的成员变量参与。
结语
学习到这里,我们了解了面向对象编程的关键概念和基础知识。在下一章节,我将会分析类的默认成员函数。
如果有什么建议或补充,亦或是错误,大家可以在评论区提出。
END