当前位置: 首页 > news >正文

面向对象编程入门:掌握C++类的基础(1/3)

面向对象编程(OOP)是一种编程范式,它使用“对象”来设计软件。在C++中,类是创建对象的蓝图。本文将介绍类的基本概念,帮助初学者理解如何在C++中使用类来实现面向对象编程。

​​​​​​​

1. 类的引入

在深入探讨类的引入之前,我们首先回顾一下编程发展的历史。最初,编程是以过程式的方式进行的,即通过一系列的函数或过程来操作数据。这种方式在处理简单问题时非常有效,但随着软件系统变得越来越复杂,过程式编程的局限性开始显现。代码的维护和更新变得困难,因为数据和操作这些数据的函数散布在整个程序中,缺乏组织性。

面向对象编程(OOP)的引入,是为了解决这些问题。OOP通过将数据和操作数据的函数封装在一起,形成一个紧密相关的单元——对象,来提高代码的重用性、灵活性和可维护性。类则是创建这些对象的蓝图

C++ 基于面向对象 的, 关注 的是 对象 ,将一件事情拆分成不同的对象,靠对象之间的交互完
成。
C 语言结构体中只能定义变量,在 C++ 中,结构体内不仅可以定义变量,也可以定义函数。 比如:
之前在数据结构初阶中,用 C 语言方式实现的栈,结构体中只能定义变量 ;现在以 C++ 方式实现,
会发现 struct 中也可以定义函数
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; // 私有成员变量
};

类的两种定义方式:

1. 声明和定义全部放在类体中,需注意:成员函数如果 在类中定义 ,编译器可能会将其当成
联函数处理。

2. 类声明放在.h文件中,成员函数定义放在.cpp文件中,注意:成员函数名前需要加类名::

同时类就是作用域    
    当进行函数的声明与定义分离时, 我们要进行 ::  域值搜索
    这样才能做到函数分离

原理如下:

在C++中,使用作用域解析运算符::实现函数声明与定义分离的底层原理,涉及到编译器如何处理类的成员函数和如何在内存中表示这些函数。虽然具体的实现细节可能因编译器而异,但基本原理保持一致。以下是一些关键点,用以解释这一过程的底层原理:

1. 名称修饰(Name Mangling)

C++支持函数重载,即允许多个同名函数存在,只要它们的参数列表不同。为了在编译后的代码中区分这些同名函数,编译器进行名称修饰(或称为名称矫正),给函数名添加额外的信息。这些信息可能包括函数所属的类名、函数的参数类型等。因此,即使两个函数原本有相同的名称,经过名称修饰后,它们在编译器内部的表示是不同的。

2. 符号表(Symbol Table)

在编译过程中,编译器会维护一个符号表,记录变量、函数等标识符及其相关信息(如类型、作用域、内存地址等)。当使用作用域解析运算符::定义类的成员函数时,编译器会将该函数与其类的作用域关联起来,并在符号表中进行相应的记录。这确保了函数定义与声明的一致性,并允许编译器正确地解析函数调用。

3. 类的访问限定符及封装

C++中的类使用访问限定符publicprivateprotected来控制成员的访问权限。public成员在任何地方都能被访问,而private成员只能被类的成员函数访问。封装是OOP的一个核心概念,它隐藏了对象的具体实现,只通过一组公开的接口与外界交互。

类的访问限定符和封装是面向对象编程的基石之一。它们共同定义了如何在类的内部和外部访问成员变量和成员函数,从而控制了类的接口和实现的可见性和可访问性。深入理解这些概念对于设计健壮和易于维护的软件系统至关重要。

访问限定符

C++提供了三种访问限定符:publicprivateprotected,它们各自有不同的访问控制级别。

  • public成员:可以被任何外部代码访问。使用这个限定符的成员定义了类的外部接口。

  • private成员:只能被该类的成员函数、友元函数和友元类访问。这是封装的核心,保证了类的内部实现的隐藏和保护。

  • protected成员:可以被该类、派生类(子类)及友元类访问,但不能被其他外部代码直接访问。这个限定符在继承中尤其有用,允许子类访问和修改继承自父类的成员。

【访问限定符说明】
1. public 修饰的成员在类外可以直接被访问
2. protected private 修饰的成员在类外不能直接被访问 ( 此处 protected private 是类似的 )
3. 访问权限 作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
4. 如果后面没有访问限定符,作用域就到 } 即类结束。
5. class 的默认访问权限为 private struct public( 因为 struct 要兼容 C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

 C++structclass的区别是什么?

解答:C++需要兼容C语言,所以C++struct可以当成结构体使用。另外C++struct还可以用来 定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是publicclass定义的类 默认访问权限是private。注意:在继承和模板参数列表位置,structclass也有区别,后序给大 家介绍

封装的原则和好处

封装不仅仅是限制对类成员的访问,它还涉及到如何组织数据和函数以定义类的行为。良好的封装可以带来以下好处:

  • 数据隐藏:通过将类的内部实现细节(如数据成员)设置为private,可以防止外部代码直接访问和修改这些数据,从而避免意外或恶意的干扰。

  • 接口和实现分离:公有接口(public成员函数)与私有实现(private数据成员和辅助函数)的分离,使得类的使用者和类的开发者能够独立工作,提高了代码的可维护性和灵活性。

  • 减少耦合:封装促进了类的独立性,减少了不同类之间的依赖关系(耦合)。这使得修改一个类的内部实现不会影响到使用该类的其他代码。

  • 增强安全性:通过严格控制对类成员的访问,可以防止数据被意外或恶意修改,增加了程序的稳定性和安全性。

封装的实现技巧

  • 最小权限原则:除非有充分的理由,否则应将类成员的访问权限设置为最严格的级别。通常,数据成员应该是private,而只有那些需要被外部访问的成员函数才设置为public

  • 使用访问函数:对于需要被外部访问的私有数据成员,提供公有的访问函数(如gettersetter)来控制对这些数据的访问,同时可以在这些函数中加入必要的逻辑检查。

  • 友元函数和友元类:当确实需要在类外部访问其私有成员时,可以考虑使用友元函数和友元类。但需谨慎使用,以避免破坏封装性。

4. 类的作用域

类的作用域决定了其中声明的变量和函数的可见性。类的成员函数可以访问同一类中的所有成员,包括私有成员。

在C++中,类的作用域是一个独立的命名空间,它包含了类的所有成员定义(包括变量、函数、类型等)。类的作用域开始于类定义的左花括号{,结束于相应的右花括号}。类内定义的成员可以在整个类作用域内被访问。

成员函数内的作用域

当在类的成员函数内部时,可以直接访问类的其他成员(包括私有成员和保护成员),无需使用任何特殊的前缀。这是因为成员函数自动获得了对类作用域内所有成员的访问权限。例如:

class MyClass {
public:void setVal(int val) { this->value = val; }int getVal() const { return value; }
private:int value;
};

在上述代码中,setValgetVal函数可以直接访问私有成员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++中,类的对象大小是一个重要的概念,它直接关系到内存的使用效率。类的对象大小不仅取决于其成员变量的类型和数量,还受到编译器的内存对齐规则的影响。了解如何计算类的对象大小以及哪些因素会影响这个大小,对于优化程序性能和内存使用至关重要。

基本原则​​​​​​​

  1. 非静态数据成员:类的对象大小主要由其非静态数据成员的大小决定。静态数据成员不计入对象大小,因为静态成员属于类本身,而不是类的任何特定对象。

  2. 空类的大小:在C++中,一个空类的对象大小为1字节。这是为了确保同一个类的两个不同对象在内存中有不同的地址。

  3. 继承:如果一个类是从其他类继承来的,那么子类对象的大小至少等于所有基类对象大小的总和,加上子类自己的非静态成员变量大小。如果有虚继承,情况会更复杂。

  4. 虚函数:如果一个类有虚函数,那么每个对象中都会有一个指向虚函数表(vtable)的指针。这个指针的大小通常是一个指针的大小(在32位系统上是4字节,在64位系统上是8字节),但具体大小取决于平台。

    结论:一个类的大小,实际就是该类中 成员变量 之和,当然要注意内存对齐
    注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象

 

7.3 结构体内存对齐规则
1. 第一个成员在与结构体偏移量为 0 的地址处。
2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
VS 中默认的对齐数为 8
3. 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
【面试题】
1. 结构体怎么对齐? 为什么要进行内存对齐?
2. 如何让结构体按照指定的对齐参数进行对齐?能否按照 3 4 5 即任意字节对齐?
3. 什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景

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++中实践面向对象编程。面向对象编程不仅能让代码更加模块化、易于理解和维护,还能提高软件开发的效率和质量。


相关文章:

  • MCU中断控制
  • CSRNET图像修复,DNN
  • http协议与apache
  • STM32_ESP8266 连接阿里云 操作图解
  • CSS中伪元素和伪类的区别和作用?
  • Vue3实现带动画效果的tab栏切换
  • Elasticsearch:创建自定义 ES Rally tracks 的分步指南
  • C++结合Lambda表达式在函数内部实现递归
  • MapboxGL JS⽀持哪些地图样式和交互控件?
  • 「数据结构」哈希表2:实现哈希表
  • MySQL高级特性篇(7)-数据库版本控制与迁移
  • SpringSecurity安全框架
  • yolov5-tracking-xxxsort yolov5融合六种跟踪算法(一)--环境配置GPU版本
  • Unity摄像机跟随
  • LocalSend跨设备传输文件传输协议 v2
  • 【mysql】环境安装、服务启动、密码设置
  • Apache的基本使用
  • css系列之关于字体的事
  • ECS应用管理最佳实践
  • ES6之路之模块详解
  • Java读取Properties文件的六种方法
  • k个最大的数及变种小结
  • React组件设计模式(一)
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 搞机器学习要哪些技能
  • 聊聊flink的BlobWriter
  • 前端
  • 驱动程序原理
  • 学习笔记TF060:图像语音结合,看图说话
  • 阿里云重庆大学大数据训练营落地分享
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • #162 (Div. 2)
  • #define、const、typedef的差别
  • #pragam once 和 #ifndef 预编译头
  • #设计模式#4.6 Flyweight(享元) 对象结构型模式
  • $HTTP_POST_VARS['']和$_POST['']的区别
  • (1)bark-ml
  • (pytorch进阶之路)扩散概率模型
  • (独孤九剑)--文件系统
  • (二)pulsar安装在独立的docker中,python测试
  • (每日持续更新)jdk api之FileReader基础、应用、实战
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (转载)虚幻引擎3--【UnrealScript教程】章节一:20.location和rotation
  • .NET Core 网络数据采集 -- 使用AngleSharp做html解析
  • .NET 的程序集加载上下文
  • .NET/C# 解压 Zip 文件时出现异常:System.IO.InvalidDataException: 找不到中央目录结尾记录。
  • .NET框架设计—常被忽视的C#设计技巧
  • .NET实现之(自动更新)
  • .NET性能优化(文摘)
  • @require_PUTNameError: name ‘require_PUT‘ is not defined 解决方法
  • [ HTML + CSS + Javascript ] 复盘尝试制作 2048 小游戏时遇到的问题
  • [BROADCASTING]tensor的扩散机制
  • [bzoj 3534][Sdoi2014] 重建
  • [C#][opencvsharp]opencvsharp sift和surf特征点匹配