面向对象编程(OOP)是一种编程范式,它使用“对象”来设计软件。在C++中,类是创建对象的蓝图。本文将介绍类的基本概念,帮助初学者理解如何在C++中使用类来实现面向对象编程。
1. 类的引入
在深入探讨类的引入之前,我们首先回顾一下编程发展的历史。最初,编程是以过程式的方式进行的,即通过一系列的函数或过程来操作数据。这种方式在处理简单问题时非常有效,但随着软件系统变得越来越复杂,过程式编程的局限性开始显现。代码的维护和更新变得困难,因为数据和操作这些数据的函数散布在整个程序中,缺乏组织性。
面向对象编程(OOP)的引入,是为了解决这些问题。OOP通过将数据和操作数据的函数封装在一起,形成一个紧密相关的单元——对象,来提高代码的重用性、灵活性和可维护性。类则是创建这些对象的蓝图
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(const DataType& data)
{
// 扩容
_array[_size] = data;
++_size;
}
DataType Top()
{
return _array[_size - 1];
}
void Destroy()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
DataType* _array;
size_t _capacity;
size_t _size;
};
int main()
{
Stack s;
s.Init(10);
s.Push(1);
s.Push(2);
s.Push(3);
cout << s.Top() << endl;
s.Destroy();
return 0;
}
2. 类的定义
类的定义是面向对象编程(OOP)的核心。在C++等面向对象的编程语言中,类不仅仅是数据和方法的集合,它还定义了一种新的数据类型。理解类的定义对于掌握面向对象编程至关重要。
如何定义一个类
在C++中,定义一个类涉及到指定类的名称、成员变量(属性)以及成员函数(方法)。这些成员变量和成员函数描述了该类的对象的状态和行为。以下是一个简单的类定义示例:
class MyClass {
public: // 公有访问修饰符
// 构造函数
MyClass() {
// 初始化代码
}
// 公有成员函数
void myFunction() {
// 函数实现
}
private: // 私有访问修饰符
int myVariable; // 私有成员变量
};
类的两种定义方式:
2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::
同时类就是作用域
当进行函数的声明与定义分离时, 我们要进行 :: 域值搜索
这样才能做到函数分离
原理如下:
在C++中,使用作用域解析运算符::
实现函数声明与定义分离的底层原理,涉及到编译器如何处理类的成员函数和如何在内存中表示这些函数。虽然具体的实现细节可能因编译器而异,但基本原理保持一致。以下是一些关键点,用以解释这一过程的底层原理:
1. 名称修饰(Name Mangling)
C++支持函数重载,即允许多个同名函数存在,只要它们的参数列表不同。为了在编译后的代码中区分这些同名函数,编译器进行名称修饰(或称为名称矫正),给函数名添加额外的信息。这些信息可能包括函数所属的类名、函数的参数类型等。因此,即使两个函数原本有相同的名称,经过名称修饰后,它们在编译器内部的表示是不同的。
2. 符号表(Symbol Table)
在编译过程中,编译器会维护一个符号表,记录变量、函数等标识符及其相关信息(如类型、作用域、内存地址等)。当使用作用域解析运算符::
定义类的成员函数时,编译器会将该函数与其类的作用域关联起来,并在符号表中进行相应的记录。这确保了函数定义与声明的一致性,并允许编译器正确地解析函数调用。
3. 类的访问限定符及封装
C++中的类使用访问限定符public
、private
和protected
来控制成员的访问权限。public
成员在任何地方都能被访问,而private
成员只能被类的成员函数访问。封装是OOP的一个核心概念,它隐藏了对象的具体实现,只通过一组公开的接口与外界交互。
类的访问限定符和封装是面向对象编程的基石之一。它们共同定义了如何在类的内部和外部访问成员变量和成员函数,从而控制了类的接口和实现的可见性和可访问性。深入理解这些概念对于设计健壮和易于维护的软件系统至关重要。
访问限定符
C++提供了三种访问限定符:public
、private
和protected
,它们各自有不同的访问控制级别。
-
public
成员:可以被任何外部代码访问。使用这个限定符的成员定义了类的外部接口。 -
private
成员:只能被该类的成员函数、友元函数和友元类访问。这是封装的核心,保证了类的内部实现的隐藏和保护。 -
protected
成员:可以被该类、派生类(子类)及友元类访问,但不能被其他外部代码直接访问。这个限定符在继承中尤其有用,允许子类访问和修改继承自父类的成员。
【访问限定符说明】1. public 修饰的成员在类外可以直接被访问2. protected 和 private 修饰的成员在类外不能直接被访问 ( 此处 protected 和 private 是类似的 )3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止4. 如果后面没有访问限定符,作用域就到 } 即类结束。5. class 的默认访问权限为 private , struct 为 public( 因为 struct 要兼容 C)注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
C++中struct和class的区别是什么?
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来 定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类 默认访问权限是private。注意:在继承和模板参数列表位置,struct和class也有区别,后序给大 家介绍
封装的原则和好处
封装不仅仅是限制对类成员的访问,它还涉及到如何组织数据和函数以定义类的行为。良好的封装可以带来以下好处:
-
数据隐藏:通过将类的内部实现细节(如数据成员)设置为
private
,可以防止外部代码直接访问和修改这些数据,从而避免意外或恶意的干扰。 -
接口和实现分离:公有接口(
public
成员函数)与私有实现(private
数据成员和辅助函数)的分离,使得类的使用者和类的开发者能够独立工作,提高了代码的可维护性和灵活性。 -
减少耦合:封装促进了类的独立性,减少了不同类之间的依赖关系(耦合)。这使得修改一个类的内部实现不会影响到使用该类的其他代码。
-
增强安全性:通过严格控制对类成员的访问,可以防止数据被意外或恶意修改,增加了程序的稳定性和安全性。
封装的实现技巧
-
最小权限原则:除非有充分的理由,否则应将类成员的访问权限设置为最严格的级别。通常,数据成员应该是
private
,而只有那些需要被外部访问的成员函数才设置为public
。 -
使用访问函数:对于需要被外部访问的私有数据成员,提供公有的访问函数(如
getter
和setter
)来控制对这些数据的访问,同时可以在这些函数中加入必要的逻辑检查。 -
友元函数和友元类:当确实需要在类外部访问其私有成员时,可以考虑使用友元函数和友元类。但需谨慎使用,以避免破坏封装性。
4. 类的作用域
类的作用域决定了其中声明的变量和函数的可见性。类的成员函数可以访问同一类中的所有成员,包括私有成员。
在C++中,类的作用域是一个独立的命名空间,它包含了类的所有成员定义(包括变量、函数、类型等)。类的作用域开始于类定义的左花括号{
,结束于相应的右花括号}
。类内定义的成员可以在整个类作用域内被访问。
成员函数内的作用域
当在类的成员函数内部时,可以直接访问类的其他成员(包括私有成员和保护成员),无需使用任何特殊的前缀。这是因为成员函数自动获得了对类作用域内所有成员的访问权限。例如:
class MyClass {
public:
void setVal(int val) { this->value = val; }
int getVal() const { return value; }
private:
int value;
};
在上述代码中,setVal
和getVal
函数可以直接访问私有成员value
,因为它们都在MyClass
的作用域内。
静态成员的作用域
静态成员变量和静态成员函数属于类本身,而不是类的任何特定对象。因此,它们在类的所有实例之间共享。静态成员可以通过类名直接访问(即使从类外部),只要它们是公有的。例如:
class MyClass {
public:
static int staticValue;
static void printStaticValue() {
std::cout << staticValue << std::endl;
}
};
int MyClass::staticValue = 10;
// 从类外部访问静态成员
MyClass::printStaticValue(); // 输出: 10
5. 类的实例化
类的实例化是面向对象编程中的一个核心概念。在C++中,实例化是指根据类定义创建对象的过程。类本身只是一个模板,而通过实例化,我们创建了一个具体的实体,这个实体包含了类定义的所有属性和方法。理解类的实例化对于有效使用面向对象的编程范式至关重要。
如何实例化一个类
在C++中,类可以通过多种方式实例化,最直接的方式是在栈上创建对象:
MyClass obj;
这里,MyClass
是类名,obj
是根据MyClass
类模板创建的对象。这种方式会自动调用类的默认构造函数(如果定义了的话)来初始化对象。
使用构造函数实例化
构造函数是一种特殊的成员函数,它在对象被创建时自动调用,用于初始化对象。如果类定义了构造函数,可以在实例化时传递参数给构造函数:
class MyClass {
public:
MyClass(int a) : value(a) {}
private:
int value;
};
MyClass obj(10);
在这个例子中,MyClass
有一个接收int
类型参数的构造函数,因此在创建obj
对象时,我们传递了一个整数值10
作为参数。
实例化的内部过程
当一个类的对象被实例化时,C++编译器会在内存中为对象分配空间,足够存储其所有的数据成员。如果对象是在栈上创建的,其生命周期会被限制在声明它的作用域内;如果对象是通过
new
在堆上创建的,它会一直存在,直到使用delete
显式删除。对于每个非静态成员变量,C++编译器会按照类定义中的顺序初始化它们。如果成员变量是对象(即类的实例),则会调用相应的构造函数进行初始化。对于静态成员变量,它们在程序启动时初始化,直到程序终止
6. 类的对象大小的计算
类的对象大小取决于其非静态数据成员的大小。编译器可能会为了内存对齐而调整对象的实际大小,这可能导致空间的额外占用。
在C++中,类的对象大小是一个重要的概念,它直接关系到内存的使用效率。类的对象大小不仅取决于其成员变量的类型和数量,还受到编译器的内存对齐规则的影响。了解如何计算类的对象大小以及哪些因素会影响这个大小,对于优化程序性能和内存使用至关重要。
基本原则
-
非静态数据成员:类的对象大小主要由其非静态数据成员的大小决定。静态数据成员不计入对象大小,因为静态成员属于类本身,而不是类的任何特定对象。
-
空类的大小:在C++中,一个空类的对象大小为1字节。这是为了确保同一个类的两个不同对象在内存中有不同的地址。
-
继承:如果一个类是从其他类继承来的,那么子类对象的大小至少等于所有基类对象大小的总和,加上子类自己的非静态成员变量大小。如果有虚继承,情况会更复杂。
-
虚函数:如果一个类有虚函数,那么每个对象中都会有一个指向虚函数表(vtable)的指针。这个指针的大小通常是一个指针的大小(在32位系统上是4字节,在64位系统上是8字节),但具体大小取决于平台。
结论:一个类的大小,实际就是该类中 ” 成员变量 ” 之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象
7.3 结构体内存对齐规则1. 第一个成员在与结构体偏移量为 0 的地址处。2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS 中默认的对齐数为 83. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
7. 类成员函数的this指针
this
指针是一个特殊的指针,它指向调用成员函数的对象本身。this
使得成员函数能够访问调用它的对象的成员。在成员函数内部,this
指针是隐含的,不需要显式传递。
1. this 指针的类型:类类型 * const ,即成员函数中,不能给 this 指针赋值。2. 只能在 “ 成员函数 ” 的内部使用3. this 指针本质上是 “ 成员函数 ” 的形参 ,当对象调用成员函数时,将对象地址作为实参传递给this 形参。所以 对象中不存储 this 指针 。4. this 指针是 “ 成员函数 ” 第一个隐含的指针形参,一般情况由编译器通过 ecx 寄存器自动传递,不需要用户传递
this
指针的基本用法
this
指针与对象的关系
通过this
指针,成员函数可以访问对象的所有成员,包括私有成员、保护成员和公有成员。这使得成员函数能够操作对象的状态,实现对象的行为。this
指针是实现面向对象特性如封装和多态的基础之一。
this
指针的底层实现
在C++编译的过程中,this
指针作为成员函数的一个隐式参数被传递。对于成员函数的调用,编译器在调用点自动将对象的地址作为this
指针传递给函数。这意味着成员函数内部对this
的使用实际上是对调用该函数的对象的操作。
- 访问成员变量:当成员变量与局部变量或参数名称冲突时,可以使用
this
指针来区分它们。class MyClass { public: MyClass(int value) { this->value = value; } int getValue() const { return this->value; } private: int value; };
在上述代码中,构造函数和
getValue
成员函数使用this->value
来访问对象的成员变量value
,以区别于任何同名的局部变量或参数。 - 实现链式调用:通过返回对象的引用或指针,
this
指针可以用于实现链式调用class MyClass { public: MyClass& setValue(int value) { this->value = value; return *this; } int getValue() const { return value; } private: int value; }; MyClass obj; obj.setValue(10).getValue(); // 链式调用
this
指针的特性 -
隐式性:
this
指针是隐式传递给每个非静态成员函数的。它不需要在函数调用时显式提供。 -
常量性:在常量成员函数中,
this
指针的类型是指向常量的指针,即const ClassName* const this
。这防止了常量成员函数修改对象的状态。 -
不可赋值:
this
指针是不可赋值的。尝试修改this
指针的值会导致编译错误。
通过学习类的这些基础知识,你可以开始在C++中实践面向对象编程。面向对象编程不仅能让代码更加模块化、易于理解和维护,还能提高软件开发的效率和质量。