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

类与对象(一)

目录

1.面向过程和面向对象初步认识

2. 类的引入

3. 类的定义

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

4.1 访问限定符

4.2 封装

5. 类的作用域

6. 类的实例化

7. 类对象模型

7.1 类对象的存储方式

7.2 结构体内存对齐规则

7.3 特殊情况:空类的大小

8. this 指针

8.1 this 指针的引出

8.2 this 指针的特性

9. C 语言和 C++ 实现 栈(Stack) 的对比

💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!

👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!


1.面向过程和面向对象初步认识

  • 面向过程(C 语言):关注解决问题的过程,分析求解步骤,通过函数调用逐步解决问题。
  • 面向对象(C++):基于面向对象编程,关注对象,将事情拆分成不同对象,依靠对象间的交互完成任务。

2. 类的引入

  • C 语言结构体:只能定义变量。
  • C++ 结构体:不仅能定义变量,还能定义函数。

示例:

#include <iostream>  
#include <cstdlib>   // 用于使用 malloc、free 和 perror 函数
#include <cassert>   

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) 
    {
        if (_size == _capacity) 
        {
            size_t newCapacity = _capacity * 2;
            
            DataType* newArray = (DataType*)realloc(_array, sizeof(DataType) * newCapacity);
            if (newArray == nullptr) 
            {
                perror("realloc申请空间失败");
                return;
            }
            _array = newArray;
            _capacity = newCapacity;
        }
        // 将元素存入栈中
        _array[_size] = data;

        ++_size;
    }

    DataType Top() 
    {
        // 确保栈不为空
        assert(_size > 0);
        // 返回栈顶元素
        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;
    // 初始化栈,指定初始容量为 10
    s.Init(10);
    
    s.Push(1);
    s.Push(2);
    s.Push(3);

    std::cout << s.Top() << std::endl;

    s.Destroy();

    return 0;
}

C++ 中,更常用 class 代替 struct 来定义类

思考:为什么是 ++_size; 而非 _size++; ?

性能方面:现代编译器中两者在性能上差异微乎其微,但理论上前置自增性能更优。后置自增需要额外内存空间保存原值,并且操作结束后还要进行一次赋值;前置自增直接对变量进行自增并返回结果,没有这些额外开销。

代码语义方面:前置自增更直观地表达“先进行自增,再更新栈元素数量”的操作顺序。


3. 类的定义

  • 定义格式class 是定义类的关键字,ClassName 是类名,类定义结束后分号不能省略。
class className 
{
    // 类体:由成员函数和成员变量组成

};  // 一定不要忘记分号

int main()
{
    classname 对象名;
    对象名.成员函数名();
}

  • 类的成员:类中的变量称为类的属性或成员变量,类中的函数称为类的方法或成员函数。
  • 两种定义方式
    • (1)声明和定义全放类体中:成员函数在类中定义,编译器可能将其当作内联函数处理。
    • (2)类声明放 .h 文件,成员函数定义放 .cpp 文件:成员函数名前需加类名 :: 。一般推荐这种方式。
  • 成员变量命名规则建议:为避免成员变量和函数形参混淆,建议给成员变量加前缀或后缀,如 _year 或 mYear

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

4.1 访问限定符

访问限定符类外访问性作用域默认访问权限
public(公有)可直接访问从出现位置开始,到下一个访问限定符出现为止;若无下一个访问限定符,则到类结束struct 定义的类默认访问权限是 public
protected(保护)不能直接访问从出现位置开始,到下一个访问限定符出现为止;若无下一个访问限定符,则到类结束
private(私有)不能直接访问从出现位置开始,到下一个访问限定符出现为止;若无下一个访问限定符,则到类结束class 定义的类默认访问权限是 private

注意:访问限定符主要是在编译阶段发挥作用,用于保证代码的安全性和规范性,而在内存中,对象的成员变量并没有访问限定符的区分。

【面试题】C++ 中 struct 和 class 的区别?

C++需要兼容C语言,所以C++ 中 struct 可当作结构体使用。C++ 中 struct 也可定义类,与 class 定义类类似,区别在于 struct 定义的类默认访问权限是 publicclass 定义的类默认访问权限是 private。在继承和模板参数列表位置也有区别。

4.2 封装

  • 面向对象三大特性:封装、继承、多态。类和对象阶段主要研究封装特性。
  • 封装定义:将数据和操作数据的方法有机结合,隐藏对象属性和实现细节,仅对外公开接口与对象交互。
  • C++ 实现封装:通过类将数据和操作方法结合,利用访问权限隐藏内部实现细节,控制类外可直接使用的方法。

5. 类的作用域

 类定义了新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需用  ::  作用域操作符指明成员所属类域。

#include <iostream>
#include <string>
using namespace std;

class Person 
{
public:
    // 声明成员函数
    void PrintPersonInfo();
    void SetName(const char* name);
    void SetGender(const char* gender);
    void SetAge(int age);
private:
    char _name[20];
    char _gender[3];
    int  _age;
};

void Person::PrintPersonInfo() 
{
    cout << _name << " " << _gender << " " << _age << endl;
}

void Person::SetName(const char* name) 
{
    // 将传入的姓名拷贝到成员变量_name中
    strncpy(_name, name, sizeof(_name) - 1);
    // 确保字符串以'\0'结尾
    _name[sizeof(_name) - 1] = '\0';
}

void Person::SetGender(const char* gender) 
{
    strncpy(_gender, gender, sizeof(_gender) - 1);
    _gender[sizeof(_gender) - 1] = '\0';
}

void Person::SetAge(int age) 
{
    _age = age;
}

int main() 
{
    Person p;
    p.SetName("Alice");
    p.SetGender("女");
    p.SetAge(25);
    p.PrintPersonInfo();

    return 0;
}

6. 类的实例化

  • 定义:用类创建对象的过程称为类的实例化。
  • 特点
    • 类是对对象的描述,是模型,定义类时未分配实际内存空间。例如学生信息表可看成类,描述具体学生信息。
    • 一个类可实例化多个对象,实例化的对象占用实际物理空间,存储类成员变量。

7. 类对象模型

7.1 类对象的存储方式

在C++中,类对象的存储方式遵循以下规则:

类对象包含成员变量,但不包含成员函数。成员函数不属于某个具体的对象,而存放在公共的代码段,所有该类的对象共享同一份成员函数代码。

综上所述,类对象的大小本质上是其成员变量所占内存空间之和。但需要考虑内存对齐规则。

7.2 结构体内存对齐规则

  • 第一个成员的起始位置:第一个成员变量存放在与结构体偏移量为0的地址处。

  • 其他成员的对齐位置:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数等于编译器默认的对齐数和该成员大小的较小值。在Visual Studio中,默认的对齐数为8。

  • 结构体的总大小:结构体总大小必须是最大对齐数(所有成员变量类型的最大者与默认对齐参数取最小)的整数倍。

  • 嵌套结构体的情况:如果嵌套了结构体,嵌套的结构体要对齐到自己的最大对齐数的整数倍处,结构体的整体大小是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。

7.3 特殊情况:空类的大小

空类:没有任何成员变量和成员函数的类,或者只有成员函数而没有成员变量的类。

编译器会给空类分配1字节的空间,目的是为了让空类的对象能够拥有唯一的地址。


8. this 指针

8.1 this 指针的引出

C++ 中,当一个类有多个对象时,每个对象都有自己独立的成员变量。但是类的成员函数是所有对象共享的。

这就产生一个问题:当一个成员函数被调用时,如何知道是哪个对象在调用它呢?

#include <iostream>
using namespace std;

class Date 
{
public:
    void Init(int year, int month, int day) 
    {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print() 
    {
        std::cout << _year << "-" << _month << "-" << _day << std::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.Init(2022, 1, 11)d2.Init(2022, 1, 12)被调用时,Init函数将无法确定将这些值赋给d1的成员变量还是d2的成员变量。

为了解决以上问题,使用 this 指针。

C++中,每个非静态的成员函数都有一个隐藏的指针参数,这个指针就是 this 指针。它的主要作用是指向当前正在调用该成员函数的对象(不可为空)。

8.2 this 指针的特性

  • this指针的类型是 类类型 * const ,这意味着 this 指针是一个指向常量的指针,不能在成员函数中给this指针赋值。例如,对于一个Student类,this指针的类型就是 Student * const 

  • this 指针只能在类的成员函数内部使用,不能在类的外部单独使用。

  • this 指针是成员函数的第一个隐含的指针形参,不需在调用成员函数时显式传递。当对象调用成员函数时,编译器会自动将对象的地址作为实参传递给 this 指针。例如调用s1.display()时,编译器会自动将其转换为Student::display(&s1)。

  • this 指针不存储于对象本身中。在函数调用期间,this 指针的值保存在函数的栈帧中。函数执行完毕后,this 指针的值就会被销毁。


9. C 语言和 C++ 实现 栈(Stack) 的对比

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int DataType;

typedef struct Stack 
{
    DataType* array;  // 用于存储栈中元素的数组指针
    int capacity;    
    int size;      // 栈中当前元素的数量   
} Stack;

// 初始化栈
void StackInit(Stack* ps) 
{
    assert(ps); 
    // 为栈的数组分配初始容量为3的空间
    ps->array = (DataType*)malloc(sizeof(DataType) * 3);
    if (NULL == ps->array) 
    {
        assert(0); 
        return;
    }
    ps->capacity = 3;  
    ps->size = 0;      
}

void StackDestroy(Stack* ps) 
{
    assert(ps);  
    if (ps->array) 
    {
        free(ps->array);  // 释放栈数组所占用的内存
        ps->array = NULL;
        ps->capacity = 0;
        ps->size = 0;
    }
}

// 检查并扩容
void CheckCapacity(Stack* ps) 
{
    if (ps->size == ps->capacity) 
    {
        int newcapacity = ps->capacity * 2;  
        DataType* temp = (DataType*)realloc(ps->array, newcapacity * sizeof(DataType));
        if (temp == NULL) 
        {
            perror("realloc申请空间失败!!!");  
            return;
        }
        ps->array = temp;  
        ps->capacity = newcapacity;  
    }
}

// 入栈
void StackPush(Stack* ps, DataType data) 
{
    assert(ps);  
    CheckCapacity(ps);  
    ps->array[ps->size] = data;  // 将元素存入栈数组
    ps->size++;  
}

int StackEmpty(Stack* ps) 
{
    assert(ps);  
    return 0 == ps->size;  // 如果栈中元素数量为0,返回1表示空,否则返回0
}

// 出栈
void StackPop(Stack* ps) 
{
    if (StackEmpty(ps))  
        return;
    ps->size--;  
}

// 获取栈顶元素
DataType StackTop(Stack* ps) 
{
    assert(!StackEmpty(ps));  
    return ps->array[ps->size - 1];  
}

// 获取栈中元素的数量
int StackSize(Stack* ps) 
{
    assert(ps);  
    return ps->size;  
}

int main() 
{
    Stack s;
    StackInit(&s);  

    StackPush(&s, 1);  
    StackPush(&s, 2);  
    StackPush(&s, 3);  
    StackPush(&s, 4);  

    printf("%d\n", StackTop(&s));  
    printf("%d\n", StackSize(&s));  

    StackPop(&s);  
    StackPop(&s);  

    printf("%d\n", StackTop(&s));  
    printf("%d\n", StackSize(&s));  

    StackDestroy(&s);  

    return 0;
}

#include <iostream>
#include <cstdlib>
#include <cstring>
using namespace std;

typedef int DataType;

class Stack 
{
public:
    // 初始化栈
    void Init() 
    {
        _array = (DataType*)malloc(sizeof(DataType) * 3);
        if (NULL == _array) 
        {
            perror("malloc申请空间失败!!!");  
            return;
        }
        _capacity = 3;  
        _size = 0;      
    }

    // 入栈
    void Push(DataType data) 
    {
        CheckCapacity();  // 检查并扩容
        _array[_size] = data;  // 将元素存入栈数组
        _size++;  
    }

    // 出栈
    void Pop() 
    {
        if (Empty())  
            return;
        _size--;  
    }

    // 获取栈顶元素
    DataType Top() { return _array[_size - 1]; }

    // 判断栈是否为空
    int Empty() { return 0 == _size; }

    // 获取栈中元素的数量
    int Size() { return _size; }

    void Destroy() 
    {
        if (_array) 
        {
            free(_array);  
            _array = NULL;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    void CheckCapacity() 
    {
        if (_size == _capacity) 
        {
            int newcapacity = _capacity * 2;  
            DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
            if (temp == NULL) 
            {
                perror("realloc申请空间失败!!!");  
                return;
            }
            _array = temp;  
            _capacity = newcapacity;  
        }
    }
private:
    DataType* _array;  
    int _capacity;     
    int _size;        
};

int main() 
{
    Stack s;
    s.Init();  

    s.Push(1);  
    s.Push(2);  
    s.Push(3);  
    s.Push(4);  

    cout << s.Top() << endl;  
    cout << s.Size() << endl;  

    s.Pop();  
    s.Pop();  

    cout << s.Top() << endl;  
    cout << s.Size() << endl;  

    s.Destroy(); 

    return 0;
}
比较维度C 语言实现C++ 实现
数据与操作的组织方式结构体中只能定义存放数据的结构,数据和操作数据方式分离通过类将数据与操作数据方式完美结合
函数参数每个函数的第一个参数都是 Stack*不需显式传递 Stack * 参数,编译器自动维护
空指针检测函数中必须要对第一个参数(Stack*)检测是否为 NULL无需手动对类似指针参数进行空指针检测,编译器自动维护
调用方式调用时必须传递 Stack 结构体变量的地址使用时如同使用自身成员
访问控制通过访问权限(public、protected、private)控制哪些方法在类外可被调用,实现封装
实现复杂度涉及大量指针操作,容易出错,实现相对复杂代码结构更清晰,一定程度上降低了因指针操作不当导致的错误风险

相关文章:

  • springcloud gateway通过数据库获取路由信息
  • 【经典算法】Leetcode-零钱兑换问题
  • [高阶技术了解]WebRPC详解
  • MongoDB Vs Elasticsearch
  • Jatpack Room 数据库封装:简洁、通用、高性能的持久化解决方案
  • 开启AI开发新时代——全解析Dify开源LLM应用开发平台
  • maven wrapper的使用
  • 【Godot4.4】写入和读取ZIP文件
  • 《灵珠觉醒:从零到算法金仙的C++修炼》卷三·天劫试炼(34)混元金斗装万物 - 0-1背包问题(二维DP)
  • React.js 基础与进阶教程
  • 【网络安全 | 漏洞挖掘】四链路账户接管
  • 视频理解之Actionclip(论文宏观解读)
  • SQL日期处理
  • Java的JDBC编程
  • BFS最短路径(十七)675. 为高尔夫比赛砍树 困难
  • 图像识别技术与应用(十六)
  • 科技工作者之家建设扬帆起航,为科技人才提供更多优质服务
  • lua C语言api学习1 编译第一个程序
  • 【巨人网络】25届春招、26届实习内推码,面经
  • nginx反向代理应用
  • 芬兰直升机相撞坠毁事故中五名人员全部遇难
  • 新城市志|GDP万亿城市,一季度如何挑大梁
  • 淮安市车桥中学党总支书记王习元逝世,终年51岁
  • 病愈出院、跳大神消灾也办酒,新华每日电讯:农村滥办酒席何时休
  • 中国物流集团等10家央企11名领导人员职务任免
  • 悬疑剧背后的女编剧:创作的差异不在性别,而在经验