在学习C++类和对象之前,我们首先需要搞清楚什么是面向过程,什么是面向对象
我们以前学的C语言就是典型的面向过程的语言。
面向过程编程是一种以过程为中心的编程方法。在这种范式下,程序被划分为一系列函数或过程,这些函数用于解决特定的问题
例如:我们可以将用手洗衣服
看作是面向过程的:
面向过程的语言有如下特点:
- 数据和函数之间通常是分离的,函数对数据进行操作,数据可以是全局的或局部的。
- 面向过程的编程语言常常使用顺序、条件语句和循环来执行任务。
C++、Java、Python等语言都是面向对象的语言。
面向对象编程是一种以对象为中心的编程方法。在这种范式下,程序被组织成一组对象,每个对象包含数据和与之相关的方法。
例如,我们可以将用洗衣机洗衣服
看作是面向对象的:
面向对象的语言有如下的特点:
- 对象是类的实例,类是定义了对象的属性和方法的蓝图。
- 面向对象编程强调数据封装、继承和多态,这些概念有助于组织和管理复杂的程序。
在C语言中,我们有struct
类型,我们称之为结构体。例如:
struct Stack
{
int* st;
int top;
};
但是,C语言的结构体有如下的局限性,这使得我们在使用时很不方便:
- 定义结构体变量时,类型名太长。例如我们要定义上面的结构体类型的变量
st1
:struct Stack st1;
- 结构体内只能声明变量,而不能声明和定义函数
为了解决这些问题,C++规定:可以在struct
里面声明和定义函数。
例如,在C++中,我们可以这样实现一个栈:
struct Stack
{
void Init(int capacity)
{
_capacity = capacity;
_st = (int*)malloc(sizeof(int) * _capacity);
_top = 0;
}
void Push(int val)
{
if (_top == _capacity)
{
_capacity *= 2;
int* tmp = (int*)realloc(_st, sizeof(int) * _capacity);
if (nullptr == tmp)
exit(-1);
_st = tmp;
}
_st[_top++] = val;
}
//仅为了展示C++struct里面可以声明和定义函数
//故其他功能不做展示
int* _st;
int _top;
int _capacity;
};
struct
关键字修饰的结构为类
class
来声明类,而不是struct
C++的类由这样的特点:
- 类名就是类型名。例如:有一个类为
class Stack
,那么就可以用这个类名定义一个变量st1:Stack st1
- 类整体定义的是一个作用域(由一对花括号
{}
包裹起来的就是一个作用域)- C++兼容C语言的绝大多数语法,可以说C++的类是C语言
struct
的升级
类的定义方法为:
//class也可以换为struct
class className
{
//类体:由成员函数和成员变量组成
}; //注意这个分号
class/struct
为定义类要用到的关键字,className
为类名- 类体中的变量称为类的属性或者成员变量,类体中的函数称为类的方法或者成员函数
方法一——将声明和定义放在一起:
例如:
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
int _year;
int _month;
int _day;
};
如果将成员函数的声明和定义都放到一起,那就需要注意:该函数可能被编译器认定为inline
内联函数
注:如果对inline
内联函数不太了解,建议看看C++特性——inline内联函数
方法二——将声明和定义分离:
如果采用”在类里面声明函数,在类外面定义函数“的方法,那就需要通过域作用限定符:
,将类名和函数名连接起来,用来说明定义的函数是这个类里面的。
例如:
在日常写代码中,我们可以将方法一和方法二结合来定义类:将复杂的、代码量大的函数定义在类外,将频繁使用的、代码简单的函数定义在类里面。这样不仅可以提高效率,而且可以提高代码的可阅读性。
我们来看一个Date
类的声明:
class Date
{
void Init(int year = 1, int month = 1, int day = 1)
{
year = year;
month = month;
day = day;
}
void Print()
{
cout << "Date-> " << year << ':' << month << ':' << day << endl;
}
int year;
int month;
int day;
};
我们声明的类成员变量为year, month, day
,成员函数Init
的三个形参也同样为year, month, day
,当我们进行赋值语句的时候,是否可以得到正确的结果呢?
我们对其初始化,并打印:
int main()
{
Date d1;
d1.Init(2023, 10, 23);
d1.Print();
return 0;
}
output:
Date-> -858993460:-858993460:-858993460
可以看到,并没有得到我们想要的结果。
因此,为了防止类似错误的出现,并提高代码的可阅读性
在C++中,我们一般将内里面的成员变量的名字前加下划线_
class Date
{
int _year;
int _month;
int _day;
};
我们同样以stack
类为例子:
class Stack
{
void Init(int capacity)
{
_capacity = capacity;
_st = (int*)malloc(sizeof(int) * _capacity);
_top = 0;
}
void Push(int val)
{
if (_top == _capacity)
{
_capacity *= 2;
int* tmp = (int*)realloc(_st, sizeof(int) * _capacity);
if (nullptr == tmp)
exit(-1);
_st = tmp;
}
_st[_top++] = val;
}
//仅为了展示C++struct里面可以声明和定义函数
//故其他功能不做展示
int* _st;
int _top;
int _capacity;
};
我们将储存数据的数组st
,栈顶指针top
,栈的最大容量capacity
及其相关方法(成员函数)放入stack
类后,
st
、top
、capacity
的内容,st
、top
、capacity
,来实现栈的功能因此为了限制用户访问类成员的权限,C++有了关键字——访问限定符
访问限定符有以下三类:
public
(公有):public
修饰的成员可以在类外直接被访问protected
(保护)、private
(私有):现阶段我们认为protected
和private
是没有区别的。被他们修饰的类成员不能在内外访问
访问限定符的作用范围:
- 从该访问限定符出现开始
- 到下一个访问限定符出现结束
例如:
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1);
private:
int _year;
int _month;
int _day;
};
Date
类里面,成员函数Init
被public
修饰,可以在类外被访问,成员变量_year
、_month
、_day
被private
修饰,不能在类外被访问。
需要清楚,如果不在类里面加访问限定符:
class
类的默认访问权限是private
struct
类的默认访问权限是public
例如:
class Date
{
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1._year = 1;
return 0;
}
//会报错:无法访问 private 成员(在“Date”类中声明)
//这就说明了:class类的默认访问权限就是private
//而如果将class改为struct,那么就可以正常运行
用类定义一个对象的过程就叫做类的实例化
需要注意:
例如:
struct Date
{
int _year = 1;
int _month;
int _day;
};
int main()
{
Date._year = 1;
//会报错:error C2059: 语法错误:“.”
return 0;
}
我们也可以将类比作是构造图,将对象比作是房子,来理清二者之间的关系:
- 构造图只是一张图纸,不会占据土地空间——类只是对成员的声明,不会开辟空间
- 由构造图建造出的房子会占用实际的土地空间——由类实例化出的对象会开辟空间来存储各成员
- 一张构造图可以建造出许多房子——一个类可以实例化多个对象
当类中没有成员函数时,类所占空间的大小遵循C语言结构体大小的计算规则:
结构体的第一个成员永远放在相较于结构体变量起始位置偏移量为0的位置
从第二个成员开始,往后的每个成员都要对齐到某个对齐数的整数倍处
- 对齐数:结构体成员自身大小和默认对齐数的较小值结构体的总大小必须是最大对齐数的整数倍
- 最大对齐数:所有成员的对齐数中的最大值
例如:
class Grade
{
int _number;
double _math;
float _chinese;
};
int main()
{
cout << sizeof(Grade) << endl;
return 0;
}
output:
24
注:如果对于结构体大小的计算不了解,建议看看C语言结构体详解
但是,如果类里面有成员函数呢?这个类的大小又是多少呢?
例如:
struct Date
{
void Init(int year = 1, int month = 1, int day = 1);
int _year;
int _month;
int _day;
};
int main()
{
cout << "sizeof(Date) -> " << sizeof(Date) << endl;
return 0;
}
要搞清楚C++类的大小到底怎么计算,我们首先就要搞清楚类对象的存储方式到底是怎么样的。
让我们来思考一个问题:
用一个类创建多个对象时,类中的成员变量需要多开辟一份吗?类中的成员函数需要多开辟一个吗?
如果想不清楚,我们仍可以用建房子来类比:
- 将房子中的卧室、厕所、厨房等私用设施比作是成员变量;将房子外的公园、亭子等公用设施比作是成员函数
- 显然,当我们用一份构造图建造多个房子时,房子的厕所、卧室肯定是要重新新建的,而房子外的公园、亭子用原来的就好
用类实例化多个对象也是如此:不同的对象所包含的成员变量为各自所有,需要重新开辟,而成员函数是这些对象共有的,不要开辟。
因此,类对象的存储方式应该是这样的:
类对象只存储成员变量,而成员函数放在公共代码区
既然类对象的成员变量并不和成员函数存放在一起,那么自然计算类的大小时,也就不需要考虑成员函数了。
所以:
struct Date
{
void Init(int year = 1, int month = 1, int day = 1);
int _year;
int _month;
int _day;
};
//Date类所占大小就是12
那么,空类的大小又是多少呢?
class Test
{
};
C++规定:空类的大小为1,这个字节不存储有效数据。用来标识定义的对象存在过
可以总结:
- 空类的大小为1个字节
- 类的大小实际上是“成员变量的大小之和”(注意内存对齐)
- 成员函数存放在公共代码区,不用计算
看下面的代码:
class Date
{
public:
void Init(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1, d2;
d1.Init(2023, 10, 23);
d2.Init();
return 0;
}
上面的代码中,我们定义了类Date
,同时示例化了两个对象d1, d2
现在就要问大家一个问题:既然这两个对象用的都是同一个Init()
函数,那编译器是怎么知道他要处理的是d1
的成员变量还是d2
的成员变量?
C++通过引入this指针来处理这个问题:
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
上面的Init()
函数和d1.Init(2023, 10, 23);
实际上等价于:
void Init(Date* this, int year = 1, int month = 1, int day = 1)
{
this->_year = year;
this->_month = month;
this->_day = day;
}
d1.Init(&d1, 2023, 10, 23);
this
指针被*const
修饰:* const this
。表示:不能修改this
指针的指向(this
至指向当前对象),但是可以修改this
指针指向空间的值(可以通过this
指针访问成员变量)this
相关的形参或实参,但是可以在类的成员函数里面显示的使用this
指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this
形参。所以对象中不存储this指针。this
指针可以为空最后,我们用一道题来结束本篇文章:
#include
using namespace std;
class Test
{
public:
void Print1()
{
cout << "Print()" << endl;
}
void Print2()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
Test* t1 = nullptr;
t1->Print1(); //Yes or Not ?
t1->Print2(); //Yes or Not ?
return 0;
}
大家认为这个程序会得到什么结果呢?
我们来进行调试:
为什么会出现这种情况呢?
我们定义了一个指向Test
类对象的指针t1
,但将其赋为空指针nullptr
函数Print1()
并没有访问类成员变量,而且成员函数存放在公共代码区,因此尽管this
指针为空,我们也可以正常使用Print1()
函数Print2()
实际上可以写为:
void Print2(Test* this)
{
cout << this->_a << endl;
}
由于this
指针为空,空指针并不指向任何有效数据,显然就会发生错误。
本次我们对类和对象进行了初步的了解和学习:知道了面向过程和面向对象的基本概念,知道了类的定义和类的实例化等相关概念和操作。
但C++类和对象的知识远不止于此。后面我们将继续学习关于类和对象的构造函数、析构函数、拷贝函数、运算符重载的知识,感兴趣的小伙伴可以订阅此专栏。
C++教程