C++----类与对象(上篇)
面向过程和面向对象的初步认识
在计算机编程中,存在两种主要的编程范式:面向过程和面向对象。这两种范式在理解和处理问题时采用了不同的方法。接下来我们将通过外卖系统的例子,来更好地理解这两种编程范式的区别。
面向过程
定义:面向过程是一种以过程或函数为中心的编程范式,它关注的是完成任务所需的步骤或过程。
外卖系统示例:在面向过程的外卖系统中,我们关注的是完成外卖业务所需的各个步骤。
- 上架:商家将菜品上架到外卖平台。
- 点餐:用户浏览菜品并选择下单。
- 派单:系统根据订单信息将订单分配给合适的骑手。
- 送餐:骑手接收订单并送餐给用户。
这些步骤被封装为一系列函数或过程,通过调用这些函数来完成整个外卖业务。
面向对象
定义:面向对象是一种以对象为中心的编程范式,它关注的是对象及其之间的关系和交互。
外卖系统示例:在面向对象的外卖系统中,我们关注的是构成外卖系统的各个对象及其之间的关系和交互。
- 商家:具有上架菜品、处理订单等属性和方法。
- 骑手:具有接收订单、送餐等属性和方法。
- 用户:具有浏览菜品、下单等属性和方法。
这些对象通过调用彼此的方法来完成外卖业务。例如,用户对象调用下单方法,系统根据订单信息调用商家对象的处理订单方法,并将订单分配给合适的骑手对象,骑手对象再调用送餐方法将餐品送给用户。
类的引入
C++中的struct与类的关系
在C语言中,struct
主要用于定义一组变量的集合,这些变量称为结构体的成员。而到了C++中,struct
被赋予了更多的功能,使其更接近类的概念。虽然C++兼容C语言,允许在C++中使用C语言中的struct
定义方式,但C++中的struct
实际上已经被“升级”,能够包含更多的特性。
C语言与C++中struct的差异
C语言中的struct:
- 只能定义变量(成员)。
- 不能包含函数。
C++中的struct:
- 可以定义变量(成员)。
- 新增功能:也可以定义成员函数(方法)。
- 默认情况下,
struct
的成员是public
的,这与C++中的class
有所不同,class
的默认成员访问权限是private
。
示例:C语言与C++中栈的实现对比
- C语言方式实现栈(只能定义变量):
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>#define MAX 100typedef struct {int top;int data[MAX];
} Stack;// 栈的操作函数需要单独定义
bool isFull(Stack* s) {return s->top == MAX - 1;
}bool isEmpty(Stack* s) {return s->top == -1;
}void push(Stack* s, int value) {if (!isFull(s)) {s->data[++s->top] = value;} else {printf("Stack is full\n");}
}int pop(Stack* s) {if (!isEmpty(s)) {return s->data[s->top--];} else {printf("Stack is empty\n");return -1; // 假设-1表示错误或无效值}
}
- C++方式实现栈(可以在struct中定义函数):
#include <iostream>
using namespace std;#define MAX 100struct Stack {int top;int data[MAX];// 成员函数bool isFull() {return top == MAX - 1;}bool isEmpty() {return top == -1;}void push(int value) {if (!isFull()) {data[++top] = value;} else {cout << "Stack is full" << endl;}}int pop() {if (!isEmpty()) {return data[top--];} else {cout << "Stack is empty" << endl;return -1; // 假设-1表示错误或无效值}}
};int main() {Stack s;s.top = -1; // 初始化栈为空s.push(10);s.push(20);cout << "Popped value: " << s.pop() << endl;cout << "Popped value: " << s.pop() << endl;return 0;
}
类的定义
class className
{//类体:由成员函数和成员变量组成
};//一定要有后面的分号
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数。
类的两种定义方式
第一种:声明和定义全部放在类体中
在这种方式中,类的声明和成员函数的定义都放在类体中。当成员函数在类体中定义时,编译器通常会将其视为内联函数。内联函数在编译时会被展开,以减少函数调用的开销。但请注意,如果成员函数较为复杂或体积较大,编译器可能会忽略内联请求。
代码示例:
#include <iostream>
using namespace std;//声明和定义全部放在类体中
class MyClass {
public:void display() { // 在类体中定义成员函数,编译器可能将其视为内联函数cout << "Hello from MyClass!" << endl;}
public:int a;char c;
};int main() {MyClass obj;obj.display(); // 调用成员函数return 0;
}
第二种:类声明放在.h文件中,成员函数定义放在.cpp文件中
在这种方式中,类的声明被放置在.h
(头文件)中,而成员函数的定义则被放置在.cpp
(源文件)中。这种方式有助于代码的模块化和重用。在.cpp
文件中定义成员函数时,需要使用类名::
语法来指定该函数属于哪个类。
此外,如果要在类中放置内联函数,那么该函数的定义和声明不能分离,即不能在类体中声明而在类体外定义,否则会出现链接错误。
代码示例:
头文件(MyClass.h):
#ifndef MYCLASS_H
#define MYCLASS_Hclass MyClass {
public:void display(); // 成员函数声明inline int add(int x, int y) { //如果要在类中放置内联函数,要在类体中声明和定义return x + y;}
}public:int a;char c;
};#endif // MYCLASS_H
源文件(MyClass.cpp):
#include <iostream>
#include "MyClass.h"
using namespace std;// 使用类名::语法定义成员函数
void MyClass::display() {cout << "Hello from MyClass in .cpp file!" << endl;
}
主程序文件(main.cpp):
#include <iostream>
#include "MyClass.h"
using namespace std;int main() {MyClass obj;obj.a = 5;obj.c = 'A';// 调用内联函数addint sum = obj.add(3, 4);cout << "The sum is: " << sum << endl; // 输出应为7obj.display(); // 调用display成员函数return 0;
}
一般情况下,推荐使用第二种定义方式。
类的访问限定
3种访问限定符
在C++中,类(class)和结构体(struct)是用户自定义的数据类型,它们可以包含数据成员(属性)和成员函数(方法)。为了控制这些成员在类外的可见性和可访问性,C++提供了三种访问限定符:public
、protected
和private
。
访问限定符说明
1. public修饰的成员在类外可以直接被访问
class MyClass {
public:int publicVar; // 公有成员变量void publicFunc() { // 公有成员函数std::cout << "Accessing public member function." << std::endl;}
};int main() {MyClass obj;obj.publicVar = 10; // 直接访问公有成员变量obj.publicFunc(); // 直接调用公有成员函数return 0;
}
2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束。
class ExampleClass {
public:int publicMember; // 公有成员protected: // 从这里开始是保护成员的作用域int protectedMember;private: // 从这里开始是私有成员的作用域int privateMember;// 如果没有其他访问限定符,下面的成员也将是私有的,直到类结束int anotherPrivateMember;
};
4.默认访问权限:在类中,如果没有明确指定访问限定符,class
的默认访问权限是private
,而struct
的默认访问权限是public
。
原因:class
旨在强调封装和隐藏实现细节,而struct
则更接近于C语言中的结构体,通常用于简单的数据封装,因此默认公开其成员。
类的封装
封装是面向对象编程的三大特性之一,与继承和多态共同构成了面向对象编程的基石。通俗来讲,封装就是把数据(属性)和方法(相关的操作)放在一起,隐藏对象的属性和实现细节(不想给别人看的就设置成私有),仅对外公开接口来和对象进行交互(可以给别人看的就设置成公有)。
封装本质上是一种管理,让用户更方便使用类。
类的作用域
类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用::作用域操作符指明成员属于哪个类域。
class Person{
public:void PrintPersonInfo();private:char _name[20];char _gender[3];int _age;
};// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{cout << _name << " "<< _gender << " " << _age << endl;
}
注意:局部域和全局域会影响生命周期,类域和命名空间域不会影响生命周期。
类的实例化
前置知识:定义与声明的区别
在C++中,定义和声明的主要区别在于是否分配了内存空间。
声明
- 声明是告诉编译器某个变量、函数或类的类型、名称和将要使用的意图。
- 声明本身不会为变量分配内存空间(除非它是全局变量或静态变量的声明,并且该声明同时是定义)。
- 对于函数和类,声明通常只提供它们的签名或接口,而不提供实现。
int a; // 在函数内部是定义(因为分配了空间),在全局或文件作用域可能是声明加定义
extern int b; // 这是一个声明,因为它没有分配空间,只是告诉编译器b在其他地方定义。
void foo(); // 这是一个函数声明,告诉编译器foo是一个返回类型为void的函数,但没有提供函数体。
定义
- 对于变量,定义是声明加上内存空间的分配;对于函数和类,定义是实现的提供。
- 当定义一个变量时,编译器会在内存中为它分配空间(对于局部变量,是在函数被调用时分配在栈上)。
- 对于函数和类,定义提供了完整的实现或成员定义。
int a = 5; // 这是一个定义,因为它不仅声明了a,还分配了空间并初始化为5。
int b; // 这也是一个定义,因为它声明了b并分配了空间(但未初始化)。
void foo() { // 这是一个函数定义,提供了foo函数的完整实现。// 函数体
}
类的实例化
当我们根据一个类的定义创建一个对象时,我们实际上是在为该对象分配内存空间,并根据类的成员变量和成员函数来初始化它。这个过程就是类的实例化,而创建出来的对象就是类的实例。
一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
//类的声明:Person类中PrintPersonInfo函数没在类中实现
class Person{
public:void PrintPersonInfo();//Person类中PrintPersonInfo函数没在类中实现private:char _name[20];char _gender[3];int _age;
};void Person::PrintPersonInfo()
{cout << _name << " "<< _gender << " " << _age << endl;
}int main()
{//类实例化对象--对象定义Person p1; // 类的实例化,创建了一个名为p1的对象Person p2; // 一个类可以实例化出多个对象p1._age = 1;//这样是可以的,因为p1是Person类实例化出的对象,已经开辟了空间//错误做法Person::_age = 1;//Person类只是声明,还没有开辟空间,不能让他的成员变量存数据return 0;
}
类对象模型
结构体内存对齐的规则
1.第一个成员在与结构体偏移量为0的地址处:
- 结构体的第一个成员总是从结构体的起始地址(偏移量为0)开始存放。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处:
- 对齐数是编译器默认的一个对齐数与该成员大小的较小值。在VS中,默认的对齐数通常为8(但可能因编译器版本或设置而异)。
- 每个成员变量的起始地址都应该是其对齐数的整数倍。为了满足这个条件,编译器可能会在成员变量之间插入填充字节。
3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍:
- 结构体的总大小应该是其所有成员变量中最大对齐数的整数倍。如果最后一个成员变量之后的内存空间不足以满足这个条件,编译器会在结构体的末尾添加填充字节。
4.如果嵌套了结构体的情况:
- 如果结构体中嵌套了另一个结构体,那么嵌套的结构体需要对其自己的最大对齐数进行对齐。
- 结构体的整体大小是包含所有成员(包括嵌套结构体)在内的最大对齐数的整数倍。
我们来看两个例子:
与C语言一样,C++也采用了内存对齐。
补充:为什么要进行内存对齐?
CPU在访问内存时,并不是逐个字节访问,而是以字长(word size)为单位进行访问。如果数据没有按照字长进行对齐,CPU可能需要多次访问内存才能读取或写入完整的数据。例如,如果CPU的字长是4字节,而一个4字节的整数存储在不是4的倍数的内存地址上,那么CPU可能需要两次访问才能读取这个整数。这会增加CPU的访问时间,降低程序的执行效率。
类对象大小的计算
在C++中,当我们定义一个类并实例化一个对象时,对象的内存空间只包含其成员变量的存储空间,而不包含成员函数。这是因为成员函数实际上是在类的公共代码段中存储的,而不是在每个对象中单独存储。
#include <iostream>
using namespace std;class MyClass {
public:int a;double b;void print() {cout << "a: " << a << ", b: " << b << endl;}
};int main() {MyClass obj;// 打印对象大小(以字节为单位)cout << "Size of MyClass object: " << sizeof(obj) << " bytes" << endl;return 0;
}
输出:Size of MyClass object: 16 bytes
在上面的代码中,MyClass
类有两个成员变量:int a
和double b
。当我们实例化MyClass
类的对象obj
时,obj
的内存大小只包括a
和b
所占用的空间,而不包括成员函数print()
所占用的空间。
为什么成员函数不存储在对象中?
如果成员函数存储在对象中,那么每次创建一个对象时,都会为该对象的成员函数分配内存。这会导致内存的大量浪费,因为不同的对象实例会拥有相同的成员函数。相反,将成员函数存储在类的公共代码段中,所有对象实例共享这些函数,从而大大节省了内存空间。
为了更好地理解类与对象的关系,我们可以使用以下比喻:
- 类:可以看做是一个小区,它定义了一套规则和设施(成员变量和成员函数)。
- 对象:可以看做是小区中的每个住户。每个住户都有自己的私人场所(成员变量),如卧室、厨房、浴室、客厅等。
- 成员变量:对应住户的私人场所,每个住户都有自己独立的空间。
- 成员函数:对应小区的公共场所,如健身房、篮球场、游泳场等。这些场所是小区所有住户共享的,而不是每个住户都拥有一个。
通过这个比喻,我们可以更直观地理解为什么成员函数不存储在对象中:因为它们是共享的,所以没有必要为每个对象都分配一个相同的函数空间。
我们再来看一种情况:当一个类中没有任何成员变量(即空类)或者只有成员函数时,编译器仍然会为这个类的对象分配至少1个字节的内存空间。这是为了能够在内存中唯一标识该对象的存在,即使它不存储任何有效数据。
#include<iostream>
using namespace std;// 类中仅有成员函数
class A1
{
public:void f1() {}
};// 类中什么都没有---空类
class A2
{
};
int main() {printf("A1: %zu bytes\n", sizeof(A1));printf("A2: %zu bytes\n", sizeof(A2));return 0;
}
输出:
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, d2;d1.Init(2022,1,11);d2.Init(2022, 1, 12);d1.Print();d2.Print();return 0;
}
输出:
我们前面讲过,函数体中没有不同对象的区分,但是d1和d2分别调用Init函数时,能针对d1和d2对象传入的参数来设置d1和d2对象,那它们是如何知道设置的是d1对象而不是d2对象呢?
其实,这是由隐藏的this实现的。在C++中,当类的成员函数被调用时,编译器会自动传递一个指向调用该函数的对象的指针,这个指针被称为this指针。
编译器会把Print函数处理成有this指针的样子:
调用的时候也会进行处理:
this指针的特性
- this指针的类型:className* const,所以在成员函数中不能给this指针赋值。
- 只能在成员函数内部使用。
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this的形参,而对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
this指针可以为空吗?
我们先来看一个代码:
// 1.下面程序编译运行结果是? 答:正常运行
class A{
public:void Print(){//虽然this是指向空的,但是函数内没有对this指针解引用cout << "Print()" << endl;}
private:int _a;
};int main()
{//这里p调用Print时不会发生解引用,因为Print地址不在对象中。p会作为实参传递给this指针。A* p = nullptr;p->Print();return 0;
}// 2.下面程序编译运行结果是? 答:运行崩溃
class A{
public:void PrintA() {//this是指向空的,但是函数内访问_a,本质是this->_acout<<_a<<endl;}
private:int _a;
};int main()
{这里p调用Print时不会发生解引用,因为Print地址不在对象中。p会作为实参传递给this指针。A* p = nullptr;p->PrintA();return 0;
}