C++都有对象了,你还没有吗?

简介: C++都有对象了,你还没有吗?

一、面向过程与面向对象

C语言作为一种面向过程的编程语言,注重解决问题的过程和步骤,通过函数和控制流程的设计来组织程序。


C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。


现实实例:制作一个简单的三明治。

321c841d47f047caab0bcc53efbffe93.png

面向过程分析:(C语言)

1.收集所需材料和工具:面包、黄油、火腿、生菜、刀子、砧板等。

2.将切好的面包放在砧板上。

3.使用刀子涂抹黄油在面包片上。

4.在其中一片面包上放上火腿和生菜。

5.将另一片面包盖在火腿和生菜上,使之成为一个完整的三明治。

6.可选:将整个三明治切成两半或四等份。

7.完成。

面向对象分析:(C++)

1.定义一个"三明治"类,它具有属性(面包、黄油、火腿、生菜)和方法(涂抹黄油、放置火腿和生菜、组装成三明治)。

2.创建一个"三明治"对象。

3.调用对象的方法,按照特定的顺序执行:

调用涂抹黄油的方法,在面包片上涂抹黄油。

调用放置火腿和生菜的方法,在其中一片面包上放置火腿和生菜。

调用组装成三明治的方法,将另一片面包盖在火腿和生菜上。

4.可选:调用切割的方法,将整个三明治切成两半或四等份。

5.完成。


在面向过程分析中,我们按照步骤逐一执行操作,强调流程和步骤的线性顺序。

而在面向对象分析中,我们将问题抽象为一个对象,该对象具有属性和方法,通过调用对象的方法来实现功能,强调对象的行为和内部状态的封装。

总之面向对象以后,重点不再关注做事的具体过程,而是关注其中涉及哪些对象

二、类

2.1 类的介绍

还记得C语言阶段学习过的结构体吧?在结构体中我们可以定义各种类型的变量,但是我们不能在结构体中定义函数.

C语言中:

7c0b6ef1fc2f49b6971057d573f986a1.png

同样一段代码在C++中,结构体内不仅可以定义变量,也可以定义函数。


C++中:

96ee71dce36140a29a8b1fa2d5e3c6a2.png

为什么呢?因为C++中将结构体升级为了==“类”.在类==中是可以定义函数的,通常被称为成员函数.

在C++中,class关键字用于定义一个类。类是一个用户定义的数据类型。

类体中内容称为类的成员:可以包含属性(成员变量)和操作/方法(成员函数)。

2.2 类的定义方式

使用class关键字可以创建一个新的类,并定义它的特征(如数据成员和成员函数)。类可以用于封装数据和行为,并提供对外部程序的接口。通过类的实例化,可以创建对象,并访问其成员变量和成员函数。在面向对象编程中,类是非常重要的一个概念,它使得程序更加模块化,易于维护和扩展。

(1)声明和定义全部放在类体中.

注意:成员函数如果在类中定义,编译器默认是按内联函数(inline)处理.(同样如果函数体过长也是不会产生内联的.)

#include <iostream>
using namespace std;//在工程代码中不建议展开可能会产生命名冲突
class Person {
public:
    // 构造函数(后面会讲,这里按普通成员函数理解)
    Person(char* n, int a) {
        name = n;
        age = a;
    }
    // 成员函数
    void introduce() {
        cout << "欢迎来到CSDN!\n我是" << name << ",我的年龄是" << age << "岁" << endl;
    }
private:
    //成员变量
    char* name;
    int age;
};
int main() {
    char name[] = "初阶牛";
    Person person(name, 18);
    person.introduce();
    return 0;
}

运行结果:

欢迎来到CSDN!
我是初阶牛,我的年龄是18岁

这个类的名字叫做 Person,它有两个私有成员变量:name 和 age。

类还有一个公有的成员函数:introduce。introduce 函数用于打印出个人信息,即打印出对象的 name 和 age 属性。

在 main 函数中,我们创建了一个名为 person 的 Person 对象,并通过构造函数初始化了它的成员变量。然后我们调用了 introduce 函数来展示个人信息。

通过使用成员函数和成员变量,我们可以对对象进行操作和访问其属性,从而使类具有更多的功能和灵活性。请注意,在 C++ 中需要使用 iostream 库进行输入输出操作,并使用 main 函数创建类的对象并调用成员函数。

(2)类的声明和"成员函数"分离

即类声明放在.h文件中,成员函数定义放在.cpp文件中.

注意:成员函数名前需要加类名::


image.png

2.3 类的访问限定符

在C++中,类的访问限定符(访问修饰符)用于控制类的成员对外部代码的可见性和访问权限。C++提供了三个主要的访问限定符:public、private和protected。

10公共访问(public):使用public关键字来指定。公共成员可以从任何地方访问,包括外部代码和其他类。公共成员在整个程序中可见。

2.私有访问(private):使用private关键字来指定。私有成员只能在声明它们的类内部访问。其他任何外部代码或其他类都无法直接访问私有成员,包括子类。

3.受保护访问(protected):使用protected关键字来指定。受保护成员只能在声明它们的类内部访问以及该类的子类中访问。外部代码无法直接访问受保护成员。

image.png


我们暂时这里将私有访问(private )和受保护访问(protected)看作相同的,后续再区分.

注意:

1.C++中class(类)的默认访问级别是私有访问(private)。类的成员将默认为私有成员,只能在类内部访问。


2.struct(结构体)为public(因为struct要兼容C语言),在C语言中,外部可以访问结构体中的成员变量.


访问限定符的选择取决于设计需求和封装原则。公共成员允许类的用户直接访问,而私有成员则隐藏了实现细节并提供了更好的封装。受保护成员专门用于派生类访问,并且在类外部不可见。

2.4 封装的介绍

封装的定义:(灰常重要)

是指将数据和方法放在一起.将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理,让用户更方便使用类。

1d67c0473cba46d8921555e6bff3f5e3.png

在现实世界中,手机是一个复杂的设备,它包含了许多内部组件和功能,如屏幕、摄像头、声音、通信等。对于我们普通用户来讲,手机只需要提供给我们我们点击的屏幕,和手动控制开关机的按键就可以了,它内部具体是怎么实现功能的我们并不关心,如果让用户去关心CPU如何设计,主板上的线路如何布局,这显然是不合理的,手机的封装也就体现了管理,帮助用户更方便的使用手机

在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

三、“类” 与 “对象” 之间的关系

我们先看一下test2函数.

class Person {
public:
    //成员函数
    Person(char* n, int a);
    void introduce();
private:
    //私有成员变量
    char* name;
    int age;
public:
    float weight;
};
void test2()
{
    //报错
    Person.weight = 60.5;//报错,类只是声明,并没有申请空间,不能用于存放数据
    //正确写法
    char name[] = "初阶牛";
    Person cjn(name, 18);//通过类实例化出 cjn这个对象.
    cjn.introduce();
    cjn.weight = 60.5;//实例化出来的对象是有空间的,可以存储数据
}

1.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;

2.一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量

有一个很形象的比喻,类就好比是建筑的图纸,类只是一些声明,并不没有去申请实际的空间,就好比图纸只是设计形状,并没有占有空间.类不能存储数据,就类似于图纸不能住人.

通过类实例化出的对象后就分配的实际空间,对象可以用于存储数据,就像图纸设计出来房子后,房子里面就可以住人了.


image.png

3.1 类的大小计算

试着猜一下下面People类的大小.

#include <iostream>
using std::cout;
using std::cin;
using std::endl;
class People
{
public:
  void Print()
  {
    int a = 0;
    double b= 1.2;
    cout << _name << endl;
  }
private:
  char _name;
  int _age;
};
int main()
{
  cout << sizeof(People) << endl;
  return 0;
}

0647ee8bd9b54775b761738d466309d3.gif

运行结果:

8

解释:

为什么是8呢?因为类在计算大小时也要考虑内存对齐.

char _name占1个字节(偏移量为0),int _age占四个字节(4-7偏移量).共八个字节.

为什么不计算成员函数的大小呢?

那就要说到类的设计方式了,因为成员函数消耗的内存相对都比较大,而每个对象都是使用同一个成员函数,如果每个对象都给成员函数开辟空间,这就比较浪费了,所以C++中的类采用下图这种方式存储:

ff7c4e9dd12c4a04b8c365b0686d0b9d.png

将;类的成员函数放在公共代码段,需要使用的时候调用即可,对象之间公用同一个成员函数.这种设计方式有效的节省了类实例化出对象后的空间消耗.


那小伙伴掌握如何计算类的大小了吗?


不妨猜一下下面A类和B类的大小.

// 只有成员函数的类
class A {
public:
  void test() {}
};
//空类
class B
{};
int main()
{
  cout << sizeof(A) << endl;
  cout << sizeof(B) << endl;
  return 0;
}

34634f1e0ab04154b7d8f0ae327f7513.gif

运行结果:

1
1

这是因为没有成员变量的类或者空类也是会在占用一个字节,因为需要占位,表示对象的存在.

3.2 this指针

#include <iostream>
using std::cout;
using std::cin;
using std::endl;
class Person {
public:
    //成员函数
    Person(char* n, int a)
    {
      _name = n;
      _age = a;
    }
    void introduce()
    {
        cout << _name << endl;
    }
private:
    //私有成员变量
    char* _name;
    int _age;
};
int main()
{
    char name1[] = "初阶牛";
    char name2[] = "CSDN";
    Person person1(name1, 18);
    Person person2(name2, 18);
    //这两个调用的是同一个函数吗?
    person1.introduce();
    person2.introduce();
  return 0;
}

运行结果:

初阶牛
CSDN

上面这段代码中这两个调用的是同一个函数吗?如果是同一个,为什么打印的结果却不一样?

    person1.introduce();
    person2.introduce();

解释:

调用的是同一个函数,之所以打印的结果不一样是因为C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针(this指针)参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。

没理解的看过来:

197bd673b91f4764abbd855f34971579.png

实就是编译器帮我们传了参,不需要用户进行手动传参.


结构体内存对齐复习:传送门

3.3 深入理解this指针

为了深入理解this指针,下面有两道题可以做一下:


第一题:this指针本身存储在哪里?

A. 栈区

B. 堆区

C. 对象中

D. 常量区

bde2bb11af3b4cebb765b6ed3f3eeea5.gif

答案:

栈区,因为this指针就是一个形参,只不过是被编译器默认传递,形参是存放在栈区的 函数栈帧建立时压栈,函数结束时,销毁.


第二题:下面这两段代码分别会出现什么情况?

//代码1:
class A
{
public:
  void Print()
  {
    cout << "HELLO CSDN" << endl;
  }
private:
  int _a;
};
int main()
{
  A* p = nullptr;
  p->Print();
  return 0;
}
// 代码2:
class A
{
public:
  void Print()
  {
    cout << _age << endl;
  }
private:
  int _age;
};
int main()
{
  A* p = nullptr;
  p->Print();
  return 0;
}

335a2f473b1140d985cfdacd19ada1cc.gif

答案:


代码1:

正常运行,虽然this是空指针,但是并没有对this指针进行解引用,传递空指针是不会报错的.


代码2:

运行崩溃,对this空指针进行解引用,属于非法访问.


运行图如下:

1fa97e3afee64122ad1de7edaeb3510f.png

四、C与C++对比

对比C语言,帮助更好的理解C++的封装特性.

C语言数据和方法是分离的,给予C程序员很大的操作空间.这样也就使得对C程序员的要求很高.太自由了!

比如:

对于一个用C语言实现的栈.很多数据在栈的外部可以被随意的修改和使用,这样就对程序员的要求极高.对于不规范的编程,(一会通过接口(函数),一会自己在外界直接访问)很容易造成混乱

C++程序员受封装的保护,对于栈中的很多操作只能通过调用对应的接口实现,更好的约束了程序员的操作规范

C实现栈:

//C语言版本
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <stdbool.h>
typedef int DataType;
typedef struct Stack
{
  DataType* array;
  int capacity;
  int size;
}Stack;
void StackInit(Stack* ps)
{
  assert(ps);
  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;
}
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;
}

C++实现栈:

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);
  printf("%d\n", s.Top());
  printf("%d\n", s.Size());
  s.Pop();
  s.Pop();
  printf("%d\n", s.Top());
  printf("%d\n", s.Size());
  s.Destroy();
  return 0;
}

7355b25fc27b47efa6584715a4fe77a3.png

最后补充一个小知识:

局部域和全局域会影响生命周期

类域和命名空间域不会影响生命周期.

目录
相关文章
|
1月前
|
编译器 C++
C++之类与对象(完结撒花篇)(上)
C++之类与对象(完结撒花篇)(上)
35 0
|
11天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
40 4
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
36 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
24 4
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
20 1
|
1月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
17 0