C++面向对象整理(1)之初识类和对象
注:整理一些突然学到的C++知识,随时mark一下
例如:忘记的关键字用法,新关键字,新数据结构
C++ 的 类和对象
提示:本文为 C++ 中 类定义、成员函数 的写法和举例
一、类的定义
类(class)就是自定义一种数据结构,这个数据结构就是更高级版本的C的结构体struct,拥有很多属性数据,但更一步相比结构体,类还新增了很多函数(称为成员函数、或方法),这些函数可以把这些属性数据当成传递的参数施加运算(如初始化赋值)或操作,而且还可以传新的参数,同时这些函数可以通过访问权限控制是否可以传入/访问这些数据。
在C++中,定义类(class)的基本语法如下:
class ClassName {
访问权限:
类内给出类的成员变量的声明
访问权限:
类内给出类的成员函数的声明或实现
};
//类外定义成员函数,使用冒号
返回类型 ClassName::成员函数的实现
注意工程上一般将类分文件编写定义,h文件中写类的声明,源文件中写类的函数的具体实现。当然也可以一次性写在一个文件里。
类的成员函数可以分为以下几种类型:
(1)普通成员函数:这是最常见的成员函数类型,它们执行类的特定操作,并且通常依赖于类的特定实例(对象)来执行。
(2)构造函数:构造函数是一种特殊的成员函数,它在创建类的对象时被自动调用,用于初始化对象的成员变量。构造函数的名字必须与类名相同,并且不能有返回类型。构造函数可以重载,可以有多个构造函数,以应对不同的初始化需求(这个非常重要,这里后面讲)。
(3)析构函数:析构函数也是特殊的成员函数,它在对象的生命周期结束时被调用,用于执行清理工作、释放分配的内存等。析构函数的名字是类名前加上波浪号 ~。
(3)赋值操作符重载函数:这是重载的赋值操作符就是等于号 = ,用于实现对象之间的赋值操作。(后面讲)
(4)取地址操作符重载函数和const修饰的取地址操作符重载函数:这些是重载的取地址操作符(&),分别用于获取对象的地址和const对象的地址。
(5)virtual的成员函数:其地址指向vtable(虚函数表)中的位置,通常用于实现多态。
(6)static的成员函数:不依赖于类的对象而存在的函数,也不依赖于类的对象而调用。关于static和const
(7)const的成员函数:表示该函数不能擅自改变成员变量的值。
1、类成员的访问权限
类的访问权限有三种:
public:公开的,表示这个成员可以在任何地方被访问。
protected:受保护的,表示这个成员可以在该类及其派生类中被其访问,但不能被该类的对象通过点运算符访问。
private:私有的,表示这个成员只能在类的内部被访问,类的外部无法直接访问。如果没有明确指定访问权限,那么默认是private。
2、类定义示例
现在,我们来定义一个坐标点类Point,包含空间三维坐标(x, y, z)
并提供提取GET或设置SET坐标值和一个求距离的接口distanceTo
(方法),假设定义的距离是两点之间的欧几里得距离。
(1)类内定义
#include <cmath> // 用于sqrt函数
class Point {
public:
Point(double x, double y, double z) {
// 这里构造函数直接赋值
x_ = x;
y_ = y;
z_ = z;
}
// 获取x坐标
double getX() const { return x_; }
// 获取y坐标
double getY() const { return y_; }
// 获取z坐标
double getZ() const { return z_; }
// 设置x坐标
void setX(double x) { x_ = x; }
// 设置y坐标
void setY(double y) { y_ = y; }
// 设置z坐标
void setZ(double z) { z_ = z; }
// 计算两点之间的欧几里得距离
double distanceTo(const Point& other) const {
double dx = x_ - other.x_;
double dy = y_ - other.y_;
double dz = z_ - other.z_;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
private:
// 坐标变量
double x_;
double y_;
double z_;
};
构造函数也可以写成以下初始化列表形式,也可用于用于初始化
Point(double x = 0.0, double y = 0.0, double z = 0.0)
: x_(x), y_(y), z_(z) {}
(2)类外定义成员函数
比如我们要将Point类分为头文件(.h)和源文件(.cpp)分开编写,类外定义不一定要分开写,只是工程上一般这样写。
首先是头文件Point.h:
// Point.h
#ifndef POINT_H
#define POINT_H
#include <cmath> // 包含cmath库以使用std::sqrt
class Point {
public:
// 构造函数,用于初始化坐标
Point(double x = 0.0, double y = 0.0, double z = 0.0);
// 获取x坐标
double getX() const;
// 获取y坐标
double getY() const;
// 获取z坐标
double getZ() const;
// 设置x坐标
void setX(double x);
// 设置y坐标
void setY(double y);
// 设置z坐标
void setZ(double z);
// 计算两点之间的欧几里得距离
double distanceTo(const Point& other) const;
private:
// 坐标变量
double x_;
double y_;
double z_;
};
#endif // POINT_H
然后是源文件Point.cpp:
// Point.cpp
#include "Point.h"
// 构造函数实现
Point::Point(double x = 0, double y = 0, double z = 0) {
// 这里直接赋值
x_ = x;
y_ = y;
z_ = z;
}
// 获取x坐标
double Point::getX() const {
return x_;
}
// 获取y坐标
double Point::getY() const {
return y_;
}
// 获取z坐标
double Point::getZ() const {
return z_;
}
// 设置x坐标
void Point::setX(double x) {
x_ = x;
}
// 设置y坐标
void Point::setY(double y) {
y_ = y;
}
// 设置z坐标
void Point::setZ(double z) {
z_ = z;
}
// 计算两点之间的欧几里得距离
double Point::distanceTo(const Point& other) const {
double dx = x_ - other.x_;
double dy = y_ - other.y_;
double dz = z_ - other.z_;
return std::sqrt(dx * dx + dy * dy + dz * dz);
}
在这个Point类中,我们定义了:
一个构造函数,用于初始化坐标值。
getX(), getY(), getZ()
方法,用于获取坐标值。
setX(), setY(), setZ()
方法,用于设置坐标值。
distanceTo()
方法,用于计算当前点到另一个点的欧几里得距离。
x_, y_, z_
是私有成员变量,只能在Point类内部访问,而公开的getX(), getY(), getZ(), setX(), setY(), setZ()
方法提供了对私有成员变量的访问接口。distanceTo()
方法使用了私有成员变量来计算距离。这样定义的类既保证了成员变量的封装性(只有类内部才能直接访问),又提供了公共接口供外部使用。
3、类对象(实例)的定义及初始化
有了自己定义好的class类型,我们就可以用它声明和初始化属于这个类的对象变量(实例 instance),比如,对于内置的数据类型有 int a = 1; int a; int b = a;
那么类该如何对应的实现上述初始化方式呢:
基于上面的类class Point来说明
于是:
(1)对应int a
;
Point p1;
执行这段代码时会立即调用定义的构造函数创建一个Point对象,坐标默认为(0, 0, 0)。
(1)对应int a = 1;
因为类有很多数据成员,但又不可能像python写作Point A ={1,2,3}
这么简单(好吧其实后面C++也引入了花括号的写法),因为编译器压根不知这是啥,所以必须引入构造函数来给每个数据成员初始化赋值:
Point p2(1.0, 2.0, 3.0); // 使用带参数的构造函数创建一个Point对象,坐标为(1.0, 2.0, 3.0)
(2)对应int b = a;
Point p3(p2); // 使用拷贝构造函数创建一个Point对象,坐标也为(1.0, 2.0, 3.0)
或者
Point p3 = p2; //若有重载的等号赋值
关于拷贝构造和重载运算符这里先不讲,记住这个语法形式即可。
(3)对应 int *p = & a;
Point * pointerP = & p3
但一般使用new关键字或智能指针在堆上动态创建对象(关于这两个详见前面关于动态内存new、关于智能指针)
Point* pointerP = new Point(7.0, 8.0, 9.0); // 在堆上动态创建一个Point对象,并返回指向它的指针
4、类的匿名对象
匿名对象(也称为临时对象)是指没有显式名称的对象。对于类来说,这意味着创建了一个类的对象,但没有给它一个变量名来用它。匿名对象通常用于那些只需要执行一次操作,如执行一次某个成员函数,并且之后不再需要引用的情况。匿名对象会在执行完构造函数或者调用成员函数后立即被销毁。一个类A
的匿名对象的格式是A()或A(paras)
。
例如,上面的Point类,我只想执行一次计算距离函数来得到两个点的距离dist
:
double dist = Point(-1,-2,-3).distanceTo(p2);//Point(-1,-2,-3)就是匿名对象
再比如有一个简单的类,它有一个输出消息的成员函数:
class MessagePrinter {
public:
void printMessage(const std::string& msg) const {
std::cout << msg << std::endl;
}
};
可以创建一个匿名对象并立即调用其成员函数,如下所示:
MessagePrinter().printMessage("Hello, World!");
本例MessagePrinter() 创建了一个 MessagePrinter 类的匿名对象,并立即调用了其 printMessage 成员函数。由于这个对象没有名称,因此它只存在于这次函数调用期间。当 printMessage 函数执行完毕后,这个匿名对象就会被销毁。
5、类对象的内存分配
成员变量和成员函数在内存中的存储和访问方式有着根本的区别。在内存中,一个类的对象通常按照成员变量的声明顺序进行布局。成员变量的内存布局可能受到内存对齐规则的影响,以确保访问效率。而成员函数则不直接占用对象的内存空间,它们的代码在程序的代码段中,成员变量是每个对象特有的,而成员函数则是类共有的。
成员变量:
成员变量是类定义中的一部分,它们存储在类的对象中。当你创建一个类的对象时,系统会在内存中为对象的每个成员变量分配空间。这些空间通常位于堆(如果使用new操作符)或栈(如果在函数内部创建对象)上。每个对象都有自己的成员变量副本,因此修改一个对象的成员变量不会影响其他对象的成员变量,除非这些对象之间通过引用或指针相互关联。
成员函数:
成员函数也是类定义中的另一部分,但它们并不直接存储在类的对象中。相反,成员函数通常存储在代码段中,这是程序的可执行部分。每个成员函数只有一个代码实例,无论创建了多少个类的对象,只有一块内存成员函数。当对象调用成员函数时,函数代码被执行,并且函数的参数(如果有的话)以及类的this指针(指向调用该函数的对象的指针)被传递给函数。
this指针(后面讲)
在成员函数内部,可以通过this指针访问调用该函数的对象的成员变量。this指针是一个隐含的指针,它指向调用成员函数的对象。通过this指针,成员函数可以访问和修改对象的成员变量。
静态成员
静态成员变量和静态成员函数是类的特殊成员。静态成员变量不属于任何特定的对象实例,而是属于类本身。它们只存储一份,而不是每个对象一份。静态成员函数也是属于类本身的,它们不能访问非静态成员变量(除非通过对象或引用),但可以直接访问静态成员变量和其他静态成员函数。