专栏:C/C++
个人主页:HaiFan.
专栏简介:本章为大家带来C++类和对象相关内容。
类和对象
- 前言
- 面向过程和面向对象
- 类的引入
- 类的定义
- 对于类中成员的命名建议
- 类的访问限定符及封装
- 访问限定符
- 封装
- 类的作用域
- 类的实例化
- 如何计算类对象的大小
- this指针
- this指针存在哪里?
- this指针可以为空吗?
前言
面向对象跟函数一样,是比较重要的内容。
那什么是对象呢?
现实生活中的对象:足球,人,手机,等一切物品都可以看作对象
程序中的对象:现实生活中具体的事物
那么把现实中的事物,转换成电脑程序的形式,所以在这里就提到了面向对象的概念
那么面向对象的好处是什么呢?
- 灵活性更高,如果代码出现问题,只需要更改出现问题的部分即可
- 易维护
- 易扩展
面向对象会涉及到:
- 类
- 对象
- 属性
- 方法
- 等等
对象:张三的手机,王五的手机。
手机就是具体的事物,而手机是对象的集合,那么就可以从对象中提取共同的特征,作为一个类别。
在这里的一个类别是手机类。
那么,能用手机干什么呢?
打电话,接电话,打游戏,刷视频等等,这些都是能用手机干的事。先称之为动作吧
手机还有品牌,颜色,大小,价格之分,这些都是通过特征去展示的。
可以通过具体的事物,来推出手机所具有共同特征和动作。
还可以分为人类。
动作:走,跑等等
特征:性别,年龄,身高,婚否等等
但是,在真正开发的时候,不会把所有的动作都列出来,而是根据需求,需要什么做什么就行了。
在程序中,把特征称为属性,把动作称为方法。
面向过程和面向对象
面向过程和面向对象是两种不同的编程范式。
面向过程编程是一种基于问题解决的编程思想,把问题看做过程或方法的集合,通过把一个大问题分解为一系列子问题的方法来解决它,刻意将问题和数据分开,重点关注数据的操作。面向过程编程中,数据和操作数据的方法是分离的,数据是被单独处理的。
而面向对象编程则是一种基于对象的编程思想,将问题看做对象之间的协作,表示成一个对象的集合,重点关注对象之间的交互和通信,以及对象的属性和方法。面向对象编程中,数据和操作数据的方法是相互关联的,它们被组合成了一个对象。
在面向对象编程中,数据和操作数据的方法总是一起的,它们封装在类的定义中,以提供更好的抽象和封装性。面向对象编程中,重点是把问题抽象成一个模型,并通过完善的类和对象来实现这个模型。
C语言就是面向过程的,关注的是过程的分析。
C++是面向对象的语言,关注的是对象。
类的引入
在C语言中,结构体是一种自定义的数据类型,可以包含不同类型的数据成员,这些数据成员将按照声明的次序在内存中依次存储。结构体可以帮助开发者组织复杂的数据结构,例如,一个由不同数据类型组成的实体对象。以下是一个简单的结构体声明:
struct student {
int id;
char name[32];
float score;
};
在C++中,结构体也是自定义的数据类型,但是具有与类相同的功能。结构体可以包括数据成员、函数成员、继承等功能。在C++中,可以像使用类一样使用结构体,如下所示:
struct Stack
{
void Init(int capacity)
{
return;
}
void Push(int data)
{
return;
}
int capacity;
int stk[100];
int _size;
};
类的定义
在C++中,可以通过定义类来创建新的数据类型。类定义了一个对象的特性、行为和方法。类可以包括数据成员、成员函数、构造函数和析构函数等。
那么类是如何定义的呢?
class className
{
....
};
class
关键字用于声明一个新的类。className
是类的名称。 {}中
可以声明类的数据成员和成员函数。
{}
中的内容称为类的成员,类中变量称为类的属性或成员变量,类中函数称为类的方法或者成员函数
类的两种定义方式
- 头文件中定义成员函数,实现文件中实现函数。
- 声明和定义全部放在类体中,需注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理
对于类中成员的命名建议
-
命名规则:采用驼峰命名法,即成员变量和成员函数的名称首字母小写,每个新单词的首字母大写,而类名和枚举类型的名称首字母大写。
-
成员变量命名:在命名成员变量时,建议使用名词来描述它所代表的数据类型,例如使用“m_”前缀来表示成员变量。
-
成员函数命名:在命名成员函数时,使用动词来描述它所执行的操作,例如使用“set”前缀来表示设定函数,“get”前缀来表示获取函数等。
-
避免使用简单的单词作为成员变量名,例如“i”、“j”等,因为这些名称可能具有多种含义,不易于理解和维护。同样的,避免使用变量名和关键字相同的名称,例如“int”、“float”,否则编译器可能无法识别变量名。
#include <iostream>
#include <cstring>
using namespace std;
class Dog
{
public:
Dog(const char name[],const char sex[],int age)
{
m_name = new char[strlen(name) + 1];
m_sex = new char[strlen(name) + 1];
strcpy(m_name, name);
strcpy(m_sex, sex);
m_age = age;
}
void set_age()
{
int age;
cin >> age;
m_age = age;
}
int get_age()
{
return m_age;
}
//......
private:
char* m_name = nullptr;
char* m_sex = nullptr;
int m_age = 0;
};
int main()
{
Dog s("二狗", "公", 2);
cout << s.get_age() << endl;
return 0;
}
类的访问限定符及封装
访问限定符
类和对象中有三种限定访问符,分别是公有(public)、私有(private)和受保护的(protected)。它们用于控制类的成员变量和成员函数的访问范围。
- 公有限定符(public)可以使得类的外部和派生类的成员函数能够直接访问到这些成员,被认为是类的“公共接口”。
- 私有限定符(private)将类的数据成员和函数封装起来,只有类的成员函数及友元函数能够访问。私有访问符用于实现信息隐藏,确保程序的稳定性和安全性。
- 受保护的限定符(protected)兼具私有和公有两者的特性,在子类中继承时可以被访问但不能被其他外部类访问,它适合在继承树中作为基类使用,被认为是类的“保护接口”。
默认情况下,如果不指定成员的访问修饰符,默认为 private 。
那么,如何使用呢?
- public在一个类中,通常将一些对外公开的接口函数和数据成员放在public区域。
- private区域中一般放置与类定义密切相关,但不希望公开给外界的函数和数据。
- protected修饰符与private修饰符相似,被声明为protected成员的变量和方法仅限于派生类和其本身内的类成员之间的使用,但是不能被其他类所访问。
#include <iostream>
using namespace std;
class Person
{
public:
int m_age; // 声明了一个 public 权限的成员变量
void eat(); // 声明了一个 public 权限的成员函数
protected:
char* m_name; // 声明了一个 protected 权限的成员变量
void sleep(); // 声明了一个 protected 权限的成员函数
private:
double m_weight; // 声明了一个 private 权限的成员变量
void work(); // 声明了一个 private 权限的成员函数
};
m_age和eat函数是公共成员,可以在类外直接被访问
Person person;
person.m_age = 19;
person.eat();
m_name和sleep函数则是保护成员,在类的派生类可以使用,但不能在其他地方进行访问
class Student: public Person {
void study() {
m_name = "Bob"; // 可以在派生类中使用
sleep(); // 可以在派生类中使用
}
};
Student student;
student.m_name = "Tom"; // 编译错误,无法访问受保护的成员
student.sleep(); // 编译错误,无法访问受保护的成
m_weight 和 work() 是私有成员,只有类本身的成员函数才能访问,外部无法访问到它们。
Person person;
person.m_weight = 60.0; // 编译错误,无法访问私有成员
person.work(); // 编译错误,无法访问私有成员
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
封装
封装是指将数据和对这些数据的操作封装在一起,形成一个类。封装机制可以隐藏类内部的具体实现细节,只向外部暴露必要的接口,从而保证程序的稳定性和安全性。使用者无需关心类内部具体实现,只需要调用公共接口即可。
以下是一个简单的类和对象实现的栈封装
stack.h文件
#pragma once
#include <iostream>
using namespace std;
typedef int StkDataType;
class Stack
{
private:
StkDataType* stk;
int capacity;
int top;
public:
void Init(int size = 4);
void push(StkDataType x);
bool empty();
int size();
void pop();
void destory();
};
stack.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "stack.h"
void Stack::Init(int size)
{
stk = new StkDataType[size];
top = -1;
capacity = size;
}
void Stack::push(StkDataType x)
{
stk[++top] = x;
}
bool Stack::empty()
{
return capacity == top + 1;
}
int Stack::size()
{
return top + 1;
}
void Stack::pop()
{
--top;
}
void Stack::destory()
{
delete stk;
capacity = 0;
top = -1;
}
main.cpp文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "stack.h"
int main()
{
Stack s;
s.Init();
s.push(1);
s.empty();
s.size();
s.pop();
s.destory();
}
上面的代码,stack类私有成员变量为栈空间stk数组,栈顶元素top和栈容量capacity,公用成员函数包括empty,push等等。
将类定义保存再了头文件stack.h中,将类成员函数的实现写在了stack.cpp中,然后在main.cpp中只需要把栈类的头文件引入一下,在定义一个Stack s对象,就可以使用了。
注:只需要包含头文件即可,不用包含栈类的实现部分,因为在编译的时候需要把stack.cpp文件一起编译成目标文件来生成可执行文件。
类的作用域
类的作用域是指类定义的可见范围。一个类的作用域可以分为两个部分声明和定义。
类的作用域规则与其他作用域规则相同(都是通过 className::成员函数
),在声明该类的作用域内,类名和成员函数,成员变量的的名称都是可见的,而在定义该类的作用域内,所有成员变量和成员函数都是可见的。
例如,在定义一个类时如果需要引用另一个类,则需要在类定义之前进行声明。另外,在不同的源文件中使用同一个类时,需要包含该类的头文件,以便编译器能够找到该类的定义。
比如上面封装内容里的栈类成员函数的实现文件中,是通过 ::来访问的,这样可以说明这个函数是这个类里的成员函数。
类的实例化
创建对象就是类的实例化
类是一个集合,比如手机类,手机有品牌,大小,颜色,型号之分,把这个公共的特征进行提取,可以封装为一个类。对象可以被看作是集合中的一个元素,是类的一个实例。通过手机类可以创建出多个不同的手机,这些创建出来的手机就是对象。
class Phone
{
private:
char* m_brand = nullptr;
double m_price = 0;
public:
void Init(const char* brand = nullptr, double price = 0)
{
m_brand = new char[(strlen(brand) + 1)];
strcpy(m_brand, brand);
m_price = price;
}
};
int main()
{
Phone a;
a.Init("C++", 999.99);
return 0;
}
可以把类理解为一个模板或者蓝图,实例化就是根据这个模板或者蓝图造成了个东西。
比如上面的代码,Phone类当成一个模板,根据这个模板创建出了a对象,a对象能进行初始化,这个初始化是模板中存在的功能,对象创建成功后,也可以使用。
如何计算类对象的大小
C语言中结构体的大小是根据内存对齐规则来计算的,以空间换取时间,而C++兼容C,那么C++的类对象的大小是如何计算的呢?
class MyClass {
public:
int num1;
float num2;
char ch;
double num3;
};
int main() {
cout << "Size of MyClass: " << sizeof(MyClass) << endl;
return 0;
}
------>输出:24
这个代码计算出结构体大小并不算难,只是基本类型的计算和内存对齐。
如果在类中添加上一些成员函数呢?成员函数的大小怎么去计算呢?
class MyClass
{
public:
int num1;
float num2;
char ch;
double num3;
void SetNum()
{
num1 = 1;
num2 = 1.1;
}
};
依旧------>输出:24
这是因为成员函数不会像成员变量一样被存储在类对象中,相反,它们被存储在代码段中的特定位置,可被所有该类的对象所共享。当执行类的成员函数时,对象只会通过一个指针来调用该函数,这个指针指向代码段中存储该函数的位置。
this指针
this指针是一个关键字,它是一个指向当前对象(调用该成员函数的对象)的指针。当类的成员函数被调用时,系统会将调用函数的对象的地址作为参数传给成员函数,并将该地址存储在this指针中。
class Person
{
public:
string name;
int age;
void Print()
{
cout << this << endl;
}
};
int main()
{
Person a, b;
cout << &a << "<=====>";
a.Print();
return 0;
}
输出结果:00DBF9F8<=====>00DBF9F8
this就是对象a的地址,通过这个地址去访问Print。
class Person
{
public:
string name;
int age;
void Print()
{
cout << "name:" << this->name << endl;
cout << "age:" << this->age << endl;
}
};
int main()
{
Person a, b;
a.age = 18;
a.name = "小明";
a.Print();
return 0;
}
输出结果:name:小明 age:18
Print成员函数使用this指针访问了a的name和age成员。
注:this 指针是一个常指针,不能被赋值。而且,this 指针只有在非静态成员函数中才有意义,在静态成员函数中不能使用 this 指针。编译器会自动处理成员函数隐含的this指针,不需要用户自己传递。
this指针存在哪里?
this指针指向当前对象的指针,存在于成员函数的局部变量中,其类型与类的类型相同
this指针可以为空吗?
答案是可以为空,但使用的时候要小心,否则会造成程序崩溃。所以必须先确保当前对象是有效的。否则在访问对象的成员时会发生不可预知的错误,比如读取或写入未定义区域的值,导致程序崩溃。
比如下面的代码
class Data
{
public:
void print()
{
cout << 1 << endl;
}
};
int main()
{
Data* p = nullptr;
p->print();
return 0;
}
这个代码能够正常运行。虽然p是null但是在成员函数print中,并没有任何对this进行的操作。
class Data
{
private:
int _a;
public:
void print()
{
cout << _a << endl;
}
};
int main()
{
Data* p = nullptr;
p->print();
return 0;
}
但是这个程序就会造成程序崩溃。
因为在调用 p 指针所指向的对象的 Print() 函数时,由于 p 是空指针,它不指向任何有效的对象,因此会产生未定义的行为,导致程序崩溃。