C++基础语法(类与对象上)

简介: C++基础语法(类与对象上)

前言:C++语法知识繁杂,要考虑的细节很多,要想学好C++一上来就啃书并不是一个很好的方法,书本的内容一般是比较严谨的,但对于初学者来说,很多概念无法理解,上来就可能被当头一棒。因此建议在学习C++之前学好C语言,再听听入门课程,C++有很多的语法概念是对C语言的一种补充,学习过C语言能更好的理解为什么要这样设计,笔者也是初学者,写的这类文章仅是用于笔记总结及对一些概念进行分析探讨,方便以后回忆,因知识有限,错误难以避免,欢迎大佬阅读指教

写类与对象这篇时,我深知要想理解类与对象,是要通过大量实践的,凭我这个初学者的三言两句,必定是错误百出,后来想想,不妨写写初学者对类与对象的理解,描述一下目前自己眼中的类与对象,分享一下自己的看法,或许能给大家提供不一样的视角,欢迎大家指教


类与对象的思想

如果学过C语言的话,你会发现,想要实现某一个项目,就要把这个项目给拆分成很多细小的,可实现的功能,然后分别写对应的函数来实现各个细小的功能,最后整体组合来实现项目,这是一个更关注过程的思想

因为每个函数都要靠你自己手动实现,你的任务就是实现每一个过程,可以想象一个一个去实现某种功能,工作量是相当的大的,把大的功能拆成一个个小的可实现的功能的过程也是相当的复杂

我们以实现日常炒菜为例,面向过程是这样的

而C++在C的基础上添加了面向对象的思想,面向对象则是把要实现的项目给抽象成不同的对象,然后各个对象之间进行交互,最后完成项目,在这个过程中,我们更关注对象之间的交互,隐藏繁琐的细节过程

同样,我们用面向对象的思想来实现日常的炒菜

首先我们根据炒菜的项目抽象出不同的对象,分别是厨师,锅,食材和佐料,盘子  

看上图,用面向对象的思想,我们就不像面向过程那样,去专注每一个过程的具体实现,而是专注于抽象出的对象之间的交互,如上面食材和锅与厨师的交互,厨师与盘子的交互,最终完成装盘,至于厨师以何种方法炒菜,什么时候添水加盖等这些繁杂的细节过程,我们是不过多考虑的

笔者在学习计组时曾听过,高级语言就是在机器码基础之上的层层抽象,从而隐藏直接操控硬件的繁杂细节,与这里的思想有些类似


类与对象的定义

C++中类的发展是在C的基础上扩充的,一开始在定义一个类的时候,采用的仍然是C语言的结构体,不过这个结构体里可以定义函数,C语言的结构体只能定义变量,C++在支持了C语言的结构体的用法之上,扩展了结构体的功能,发展出了类

#include<iostream>
using namespace std;
struct Test
{
  int num1;
  int num2;
  int Add(int x, int y)
  {
    return x + y;
  }
};
int main()
{
  struct Test T;
  T.num1 = 10, T.num2 = 20;
  int tmp = T.Add(T.num1, T.num2);
  cout << tmp << endl;
  return 0;
}

我们可以用 struct 来定义C++中的类,为了更好的做出区分,C++提供了新的定义类的关键字 class ,这两者都可以用来定义类,但以后我们还是要习惯使用 class 来定义类,用class定义一个类,在用法上和结构体蛮相似的

定义类的格式为 class   classname     class表示这是一个类,classname是你要定义的类的类名

下面,我们定义一下栈的类,名字就叫 Stack,先不要管public 和 private,后面会详细说

class Stack
{
public:
  void StackPush(int x);
  void StackPop();
  int  StackTop();
  bool StackEmpty();
private:
    int* _pos;
  int _size;
  int _capacity;
};

以上就是简单的定义一个栈的类,把栈所需要的变量数据以及行为函数都放到栈里,其中类中的变量叫成员变量或类的属性,类中的函数叫成员函数或类的方法。上面我们只是对类中的函数进行了声明,当然我们可以在类中定义这些函数,但是这样的话,整个类看起来就比较臃肿,不利于阅读,因此我们一般把函数定义到函数实现的文件中,不过定义在函数实现文件中的函数有很多,我该怎么知道定义的这个函数是不是属于某一个类的呢?

C++采用了 classname :: 域作用符来表明某个函数是属于 classname 这个类域的,下面通过一段代码来看看两种定义类的成员函数的方法

定义方法1 类的成员函数定义在其他文件里
void Stack::StackPush(int x)
 {
   if (_size == _capacity)
   {
     int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
     int* tmp = (int*)realloc(_pos, sizeof(int) * newcapacity);
     _pos = tmp;
     _capacity = newcapacity;
   }
   _pos[_size++] = x;
 }
 void Stack::StackPop()
 {
   if (_size <= 0)
     return;
   _size--;
 }
 int Stack::StackTop()
 {
   if (_size <= 0)
     std::cout << "Stack Empty" << std::endl;
   return _pos[_size-1];
 }
 bool Stack::StackEmpty()
 {
   if (_size <= 0)
     return true;
   return false;
 }
定义方法2 在类的成员函数定义在类的内部
class Stack
{
public:
void StackPush(int x)
 {
   if (_size == _capacity)
   {
     int newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
     int* tmp = (int*)realloc(_pos, sizeof(int) * newcapacity);
     _pos = tmp;
     _capacity = newcapacity;
   }
   _pos[_size++] = x;
 }
 void StackPop()
 {
   if (_size <= 0)
     return;
   _size--;
 }
 int StackTop()
 {
   if (_size <= 0)
     std::cout << "Stack Empty" << std::endl;
   return _pos[_size-1];
 }
 bool StackEmpty()
 {
   if (_size <= 0)
     return true;
   return false;
 }

需要注意的是,定义在类内部的成员函数,编译器可能会将其当作内联函数来处理


类的访问限定符

前面在介绍定义类的时候,我们提到过public 和 private,接下来我就探索一下这两个关键字到底是什么东东,先说结论,这两个关键字都是类的访问限定符,通过字面意思大家可能会猜到这个与隐私有些关系,简单来说就是相当于一把锁,把不想给你看的东西给锁起来,想给你看的就公开

就拿我们之前用类定义的栈来说,其中的成员变量记录栈的大小,栈中元素的个数,这个数据是不能乱改的,万一有人在访问类中成员时,一不小心修改了类中的成员变量,原先栈中只有10个元素,一改就成了20个了,这会导致整个栈都崩溃

所以我们在定义类的时候,希望有些成员被保护起来,不允许使用者访问,这个时候就能用到关键字private来修饰这些成员,表示这些成员都是受保护的,出了类就不能够访问这些成员了

而public修饰的成员在类外可以直接被访问,如出栈,入栈,查看栈顶元素等成员函数

访问限定符的作用域:

访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止

如果后面没有访问限定符,作用域就到块结尾即 }, 则类结束

只有该类中的成员才可以使用被 private修饰的成员,出了类,就没法直接访问

之前提到过C++的 struct 和 class 都可以定义类,这两种定义方法的区别之一就体现在访问限定符上,如果不明确给出限定访问符,用 struct 定义的类,它的成员默认都是public即公共的,用 class 定义的类,它的成员默认都是private即私有受保护的


类的实例化及大小计算

类是一种自定义类型,在定义一个类时,就相当于画一张设计图,它是没有实体的,不占有内存的,它的成员变量都是未定义的,这点和 struct 很像,只有在创建出一个类时,这个类才会被实例化,类中的成员变量才会被创建,才占有实际的内存空间

那类占的内存大小该如何计算呢?类中可以定义函数,可以定义变量,想必一个类所占的空间应该不小吧,不过事实并非如此,类的实际大小只与类中成员变量的大小有关,因为类中的成员函数是不存在类中的,而是存在公共代码区,类中成员变量的内存分配同样要遵守内存对齐规则,如果忘了,可以回顾一下C语言中结构体的内存对齐规则,类的大小计算与其相同,这里就不再赘述了


this指针

可能大家对上面类的定义还有疑问,比如,为什么我在定义类中成员变量的时候,在前面加上一个 '_' 呢?还有就是为什么在定义乘员函数时没有传指针过去就可以直接操控栈呢,C语言就需要传指针过去,否者没办法操作呀,C++是怎么做到,不传指针就能操控整个栈呢?这就是我们接下来要了解的 this 指针

事实上,C++并不是不需要传指针,而是将这个指针隐藏在参数中了,正因为C语言实现栈时,每个函数都要传指针过去,手写挺麻烦的,C++便将要传的指针给隐藏起来,这个指针的名字叫this指针

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

this 指针的类型就是 类的类型,this指针是被 const 修饰不能更改的

this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给

this形参,所以对象中不存储this指针

this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递

文字可能不是很好理解,可以看看下面这个图

上面这个例子能帮我们较好的理解类的实例化和this指针,第一个程序是能够正常运行的,尽管类的指针P是一个空指针,但是函数并不是存放到类中的,P调用函数时,会去公共代码区调用该函数,这里并没有发生解引用,是不是空指针并没关系

但是第二个程序就不行了,程序会中途崩溃,原因是成员变量_a是存放到类中的,P在调用函数时,编译器会把P的地址传给this指针,然后this指针解引用_a,此时的this指针是一个空指针,当然不能发生解引用,所以程序崩溃


目录
相关文章
|
23天前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
21 4
|
23天前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
20 4
|
23天前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
17 1
|
24天前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
15 0
|
28天前
|
编译器 C++ 数据库管理
C++之类与对象(完结撒花篇)(下)
C++之类与对象(完结撒花篇)(下)
28 0
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
51 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
18 1
下一篇
无影云桌面