从C向C++3——类和对象

一.前言

​ 类和对象是 C++的重要特性,它们使得 C++ 成为面向对象的编程语言,可以用来开发中大型项目,本节重点讲解类和对象的语法。

​ 类是创建对象的模板,一个类可以创建多个对象,每个对象都是类类型的一个变量;创建对象的过程也叫类的实例化。每个对象都是类的一个具体实例,拥有类的成员变量和成员函数。

有些教程将类的成员变量称为类的属性(Property),将类的成员函数称为类的方法(Method)。在面向对象的编程语言中,经常把函数(Function)称为方法(Method)。

​ 与结构体一样,类只是一种复杂数据类型的声明,不占用内存空间。而对象是类这种数据类型的一个变量,或者说是通过类这种数据类型创建出来的一份实实在在的数据,所以占用内存空间。

二.定义

1.最简单的类

​ 类是用户自定义的类型,如果程序中要用到类,必须提前说明,或者使用已存在的类(别人写好的类、标准库中的类等),C++语法本身并不提供现成的类的名称、结构和内容。

​ 在此之前我们或多或少的接触过这些方面的知识,直接看一个例子:

class Student{
public:
    //成员变量
    char *name;
    int age;
    float score;
    //成员函数
    void say(){
        cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
    }
};

class是 C++ 中新增的关键字,专门用来定义类。Student是类的名称;类名的首字母一般大写,以和其他的标识符区分开。{ }内部是类所包含的成员变量和成员函数,它们统称为类的成员(Member);由{ }包围起来的部分有时也称为类体,和函数体的概念类似。public也是 C++ 的新增关键字,它只能用在类的定义中,表示类的成员变量或成员函数具有“公开”的访问权限,这个到后面类的继承中在细说。

注意在类定义的最后有一个分号;,它是类定义的一部分,表示类定义结束了,不能省略。

​ 整体上讲,上面的代码创建了一个 Student 类,它包含了 3 个成员变量和 1 个成员函数。

​ 类只是一个模板(Template),编译后不占用内存空间,所以在定义类时不能对成员变量进行初始化,因为没有地方存储数据。只有在创建对象以后才会给成员变量分配内存,这个时候就可以赋值了。

2.创建对象

有了 Student 类后,就可以通过它来创建对象了,例如:

Student xqs;  //创建对象

Student是类名,xqs是对象名。

在创建对象时,class 关键字可要可不要,但是出于习惯我们通常会省略掉 class 关键字:

class Student xqs;  //正确
Student xqs;  //同样正确

除了创建单个对象,还可以创建对象数组:

Student allStu[100];

该语句创建了一个allStu数组,它拥有100个元素,每个元素都是 Student 类型的对象。

3.访问类的成员

创建对象以后,可以使用点号.来访问成员变量和成员函数,这和通过结构体变量来访问它的成员类似,如下所示:

    //创建对象
    Student stu;
    stu.name = "小明";
    stu.age = 15;
    stu.score = 98.5f;
    stu.say();

4.使用对象指针

C语言中经典的指针在 C++ 中仍然广泛使用,尤其是指向对象的指针,没有它就不能实现某些功能。

上面代码中创建的对象 stu 在栈上分配内存,需要使用&获取它的地址,例如:

Student stu;
Student *pStu = &stu;

pStu 是一个指针,它指向 Student 类型的数据,也就是通过 Student 创建出来的对象。

当然,你也可以在堆上创建对象,这个时候就需要使用前面的new关键字,例如:

Student *pStu = new Student;

这个应该好理解,类比结构体指针。

有了对象指针后,可以通过箭头->来访问对象的成员变量和成员函数,这和通过结构体指针来访问它的成员类似,请看下面的示例:

pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();

第二部分介绍了两种创建对象的方式:一种是在栈上创建,形式和定义普通变量类似;另外一种是在堆上使用 new 关键字创建,必须要用一个指针指向它,要记得 delete 掉不再使用的对象。

5.成员函数

​ 类的成员函数也和普通函数一样,都有返回值和参数列表,它与一般函数的区别是:成员函数是一个类的成员,出现在类体中,它的作用范围由类来决定;而普通函数是独立的,作用范围是全局的,或位于某个命名空间内。

​ 在类体中定义了成员函数,你也可以只在类体中声明函数,而将函数定义放在类体外面:

class Student{
public:
    //成员变量
    char *name;
    int age;
    float score;
    //成员函数
    void say();  //函数声明
};
//函数定义
void Student::say(){
    cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}

​ 在类体中直接定义函数时,不需要在函数名前面加上类名,因为函数属于哪一个类是不言而喻的。但当成员函数定义在类外时,就必须在函数名前面加上类名予以限定。::被称为域解析符(也称作用域运算符或作用域限定符),用来连接类名和函数名,指明当前函数属于哪个类。

在类体中和类体外定义成员函数的区别:

​ 在类体中和类体外定义成员函数是有区别的:在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。当然,在类体内部定义的函数也可以加 inline 关键字,但这是多余的,因为类体内部定义的函数默认就是内联函数。

​ 内联函数一般不是我们所期望的,它会将函数调用处用函数体替代,所以我建议在类体内部对成员函数作声明,而在类体外部进行定义,这是一种良好的编程习惯,实际开发中大家也是这样做的。

如果你既希望将函数定义在类体外部,又希望它是内联函数,那么可以在定义函数时加 inline 关键字。

三.访问权限以及封装

1.访问权限

​ C++通过 public、protected、private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。所谓访问权限,就是你能不能使用该类中的成员。

C++ 中的 public、private、protected 只能修饰类的成员,不能修饰类,C++中的类没有共有私有之分。

在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。

在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

访问权限:

public公共 类内可以访问 类外可以访问
protected保护 类内可以访问 类外不可以访问
privite私有 类内可以访问 类外不可以访问
#include <iostream>
using namespace std;

//类的声明
class Student{
private:  //私有的
    char *m_name;
    int m_age;
    float m_score;

public:  //共有的
    void setname(char *name);
    void setage(int age);
    void setscore(float score);
    void show();
};

//成员函数的定义
void Student::setname(char *name){
    m_name = name;
}
void Student::setage(int age){
    m_age = age;
}
void Student::setscore(float score){
    m_score = score;
}
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}

int main(){
    //在栈上创建对象
    Student stu;
    stu.setname("小明");
    stu.setage(15);
    stu.setscore(92.5f);
    stu.show();

    //在堆上创建对象
    Student *pstu = new Student;
    pstu -> setname("李华");
    pstu -> setage(16);
    pstu -> setscore(96);
    pstu -> show();

    return 0;
}

像这里的学生类,在main函数里我们不能在通过stu.name="小米"来直接访问它的name属性,只能通过公共的方法setname来访问。

  • 成员变量大都以m_开头,这是约定成俗的写法,不是语法规定的内容。以m_开头既可以一眼看出这是成员变量,又可以和成员函数中的形参名字区分开。
  • 类的声明和成员函数的定义都是类定义的一部分,在实际开发中,我们通常将类的声明放在头文件中,而将成员函数的定义放在源文件中。

setname() 为例,如果将成员变量m_name的名字修改为name,那么 setname() 的形参就不能再叫name了,得换成诸如name1_name这样没有明显含义的名字,否则name=name;这样的语句就是给形参name赋值,而不是给成员变量name赋值。

2.封装

​ private 关键字的作用在于更好地隐藏类的内部实现,该向外暴露的接口(能通过对象访问的成员)都声明为 public,不希望外部知道、或者只在类内部使用的、或者对外部没有影响的成员,都建议声明为 private。

​ 根据C++软件设计规范,实际项目开发中的成员变量以及只在类内部使用的成员函数(只被成员函数调用的成员函数)都建议声明为 private,而只将允许通过对象调用的成员函数声明为 public。

​ 我们可以额外添加两个 public 属性的成员函数,一个用来设置成员变量的值,一个用来获取成员变量的值。上面的代码中,setname()、setage()、setscore() 函数就用来设置成员变量的值;如果希望获取成员变量的值,可以再添加三个函数 getname()、getage()、getscore()

​ 给成员变量赋值的函数通常称为 set 函数,它们的名字通常以set开头,后跟成员变量的名字;读取成员变量的值的函数通常称为 get 函数,它们的名字通常以get开头,后跟成员变量的名字。

​ 这种将成员变量声明为 private、将部分成员函数声明为 public 的做法体现了类的封装性。所谓封装,是指尽量隐藏类的内部实现,只向用户提供有用的成员函数。

3.补充说明

​ 声明为 private 的成员和声明为 public 的成员的次序任意,既可以先出现 private 部分,也可以先出现 public 部分。如果既不写 private 也不写 public,就默认为 private。

​ 在一个类体中,private 和 public 可以分别出现多次。每个部分的有效范围到出现另一个访问限定符或类体结束时(最后一个右花括号)为止。但是为了使程序清晰,应该养成这样的习惯,使每一种成员访问限定符在类定义体中只出现一次。

4.内存说明

​ 类是创建对象的模板,不占用内存空间,不存在于编译后的可执行文件中;而对象是实实在在的数据,需要内存来存储。对象被创建时会在栈区或者堆区分配内存。直观的认识是,如果创建了 10 个对象,就要分别为这 10 个对象的成员变量和成员函数分配内存,如下图所示:
在这里插入图片描述

​ 不同对象的成员变量的值可能不同,需要单独分配内存来存储。但是不同对象的成员函数的代码是一样的,上面的内存模型保存了 10 份相同的代码片段,浪费了不少空间,可以将这些代码片段压缩成一份。事实上编译器也是这样做的,编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码。如下图所示:
在这里插入图片描述

所以值得注意的是,如果用sizeof()函数来计算一个类的大小时,只计算其成员变量的大小,而不考虑其成员函数的大小,当然也存在计算机组成原理中的边界对齐问题,就业笔试题目中会遇到类似问题。

四.案例练习一

1.题目

要求:

  • 设计一个立方体类(Cube),包含长m_l、宽m_w、高m_h三种成员属性
  • 求出立方体的面积和体积
  • 分别用全局函数和成员函数判断两个立方体是否相等

判断两个立方体相等的方法:长宽高分别对应相等。

2.代码

#include <iostream>
#include <string>
//声明命名空间std
using namespace std;

class Cube {
private:
    //成员变量
    int m_l;
    int m_w;
    int m_h;
public:
    //成员函数
      void SetCube(int length, int width, int high) {
        m_l = length;
        m_w = width;
        m_h = high;
    }
      int get_l() {
          return m_l;
    }
      int get_w() {
          return m_w;
      }
      int get_h() {
          return m_h;
      }

    int S() {          //计算面积
        return 2 * m_l * m_w + 2 * m_l * m_h + 2 * m_w * m_h;
    }

    int V() {         //计算体积
        return m_l * m_w * m_h;
    }
    //利用成员函数进行判断
    bool issamebyclass(Cube c) {
        if (m_l == c.get_l() && m_w == c.get_w() && m_h == c.get_h())
            return true;
        else
            return false;
    }
};

//利用全局函数判断是否相等
bool issame(Cube &c1, Cube &c2) {
    if (c1.get_h() == c2.get_h() && c1.get_l() == c2.get_l() && c1.get_w() == c2.get_w())
        return true;
    else
        return false;
}

int main() {
    Cube num1,num2;
    num1.SetCube(3, 3, 3);
    int s1 = num1.S();
    int v1 = num1.V();
    cout << "第一个立方体的面积为:" << s1 << ";体积为:" << v1 << endl;
    num2.SetCube(5, 4, 3);
    int s2 = num2.S();
    int v2 = num2.V();
    cout << "第二个立方体的面积为:" << s2 << ";体积为:" << v2 << endl;
    return 0;
}

本代码仅供参考,很多细节不完善。

五.案例练习二

1.题目

要求:假设在二维坐标系中,设计一个圆类和一个点类,判断一个圆和一个点的位置关系(圆内、圆外、圆上)

  • 圆类包含圆心(m_x,m_y)、半径r的成员属性
  • 点类包含(p_x,p_y)的成员属性
  • 判断方法:计算点到圆心的距离,与半径进行比较

2.代码

#include <iostream>
#include <string>
#include "point.h"
//声明命名空间std
using namespace std;

//点类
class Point {
private:
    int p_x;
    int p_y;
public:
    void SetPoint(int x, int y) {
        p_x = x;
        p_y = y;
    }

    int get_x() {
        return p_x;
    }
    int get_y() {
        return p_y;
    }
};

class Circlu {
private:
    Point m_p;
    int m_r;
public:
    //成员函数
    void SetCirclu(Point p,int r) {
        m_p = p;
        m_r = r;
    }
    int get_r() {
        return m_r;
    }
    Point get_center() {
        return m_p;
    }
};

//判断函数
void judge(Circlu& c, Point& p) {
    int x1 = p.get_x();
    int y1 = p.get_y();
    int x2 =(c.get_center()).get_x();
    int y2 = (c.get_center()).get_y();
    int come = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
    int pux = c.get_r() * c.get_r();
    if (come == pux)
        cout << "点在圆上" << endl;
    else if (come > pux)
        cout << "点在圆外" << endl;
    else
        cout << "点在圆内" << endl;
}


int main() {
    Point p,q;
    p.SetPoint(10, 0);
    q.SetPoint(0, 0);
    Circlu c;
    c.SetCirclu(q, 10);
    judge(c, p);
    return 0;
}

在一个.cpp文件里确实完成了上述功能,但是在大的项目中,这样写是肯定不行的,我们需要进行多文件编程。

六.结合VS进行多文件编程

1.增加.h头文件

point.h文件:

#pragma once
#include <iostream>
using namespace std;

//点类
class Point {
private:
    int p_x;
    int p_y;
public:
    void SetPoint(int x, int y);
    int get_x();
    int get_y();
};

2.增加类.cpp文件

point.cpp文件

#include "point.h"


    void Point::SetPoint(int x, int y) {
        p_x = x;
        p_y = y;
    }

    int Point::get_x() {
        return p_x;
    }
    int Point::get_y() {
        return p_y;
    }

3.改写main.cpp文件

main.cpp文件:

#include <iostream>
#include <string>
#include "point.h"
//声明命名空间std
using namespace std;



class Circlu {
private:
    Point m_p;
    int m_r;
public:
    //成员函数
    void SetCirclu(Point p,int r) {
        m_p = p;
        m_r = r;
    }
    int get_r() {
        return m_r;
    }
    Point get_center() {
        return m_p;
    }
};

//判断函数
void judge(Circlu& c, Point& p) {
    int x1 = p.get_x();
    int y1 = p.get_y();
    int x2 =(c.get_center()).get_x();
    int y2 = (c.get_center()).get_y();
    int come = (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2);
    int pux = c.get_r() * c.get_r();
    if (come == pux)
        cout << "点在圆上" << endl;
    else if (come > pux)
        cout << "点在圆外" << endl;
    else
        cout << "点在圆内" << endl;
}


int main() {
    Point p,q;
    p.SetPoint(10, 0);
    q.SetPoint(0, 0);
    Circlu c;
    c.SetCirclu(q, 10);
    judge(c, p);
    return 0;
}

这里只是改写了Point类,我们还可以继续改写Circlu类,留作练习。

相关推荐

  1. C++对象3(未完成)

    2024-01-07 04:14:05       9 阅读
  2. C++ 对象

    2024-01-07 04:14:05       9 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-01-07 04:14:05       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-01-07 04:14:05       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-01-07 04:14:05       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-01-07 04:14:05       20 阅读

热门阅读

  1. 面试 Vue 框架八股文十问十答第四期

    2024-01-07 04:14:05       43 阅读
  2. kubectl命令中常用的缩写

    2024-01-07 04:14:05       32 阅读
  3. vue3对比vue2是怎样的

    2024-01-07 04:14:05       33 阅读
  4. FreeRTOS学习笔记

    2024-01-07 04:14:05       32 阅读
  5. 编程笔记 html5&css&js 019 HTML实体

    2024-01-07 04:14:05       32 阅读
  6. 雪球股票数据接口

    2024-01-07 04:14:05       38 阅读
  7. 沙特saber认证是什么,怎么办理的

    2024-01-07 04:14:05       36 阅读
  8. MFC:CDC 类与成员

    2024-01-07 04:14:05       35 阅读
  9. 音频筑基:巴克谱和梅尔谱辨析

    2024-01-07 04:14:05       40 阅读
  10. 读书之深入理解ffmpeg_简单笔记2(初步)

    2024-01-07 04:14:05       40 阅读
  11. 如何在 C# 12 中使用主构造函数

    2024-01-07 04:14:05       29 阅读