C++ 系列 -- 类和对象

定义类

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

cpp 复制代码
class Student{
public:
    //成员变量
    char *name;
    int age;
    float score;

    //成员函数
    void say(){
        cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
    }
};
  • class是 C++ 中新增的关键字,专门用来定义类
  • Student是类的名称;类名的首字母一般大写,以和其他的标识符区分开
  • { }内部是类所包含的成员变量和成员函数,它们统称为类的成员(Member)
  • { }包围起来的部分有时也称为类体,和函数体的概念类似
  • public也是 C++ 的新增关键字,它只能用在类的定义中,表示类的成员变量或成员函数具有"公开"的访问权限

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

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

创建对象

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

cpp 复制代码
Student stu;  // 创建对象
Student *pStu = new Student; // 使用 new 关键字创建对象

两者区别是:

  • 通过类创建的对象这个变量是在栈内存
  • 通过 new 关键字创建的对象这个变量是在堆内存

更常见的是使用 new 关键字创建对象,关于此内容下面会详解

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

cpp 复制代码
Student allStu[100];

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

访问类的成员

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

cpp 复制代码
#include <iostream>
using namespace std;

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

int main(){
    Student stu; // 创建对象
    stu.name = "小明";
    stu.age = 15;
    stu.score = 92.5f;
    stu.say(); // 调用类的成员函数

    return 0;
}

运行结果:

小明的年龄是15,成绩是92.5

stu 是一个对象,占用内存空间,可以对它的成员变量赋值,也可以读取它的成员变量

使用对象指针访问

创建对象指针

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

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

cpp 复制代码
Student stu;
Student *pStu = &stu;

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

使用 new 关键字创建对象指针

当然,你也可以在堆上创建对象,这个时候就需要使用前面讲到的new关键字(C++ new和delete运算符简介),例如:

cpp 复制代码
Student *pStu = new Student;

使用 new 在堆上创建出来的对象是匿名 的,没法直接使用,必须要用一个指针指向它 ,再借助指针来访问它的成员变量或成员函数

使用对象指针访问

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

cpp 复制代码
pStu -> name = "小明";
pStu -> age = 15;
pStu -> score = 92.5f;
pStu -> say();

delete 及时释放内存

栈内存是程序自动管理的,不能使用 delete 删除在栈上创建的对象;堆内存由程序员管理,对象使用完毕后可以通过 delete 删除。在实际开发中,new 和 delete 往往成对出现 ,以保证及时删除不再使用的对象,防止无用内存堆积

cpp 复制代码
#include <iostream>
using namespace std;

class Student{
public:
    char *name;
    int age;
    float score;

    void say(){
        cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
    }
};

int main(){
    Student *pStu = new Student;
    pStu -> name = "小明";
    pStu -> age = 15;
    pStu -> score = 92.5f;
    pStu -> say();
    delete pStu;  // 删除对象

    return 0;
}

C++ 没有垃圾回收机制

C++ 是一种没有内置垃圾回收机制的语言。在 C++ 中,内存的分配和释放都是由程序员手动管理的。这意味着,程序员需要显式地分配内存、释放内存,并确保不会出现内存泄漏或悬空指针等问题。

所以为了管理内存,在实际开发中,new 和 delete 往往成对出现 ,以保证及时删除不再使用的对象,防止无用内存堆积

需要注意的是,如果程序员没有显式地释放动态分配的内存空间,就会出现内存泄漏的问题。此外,如果程序员在指针被释放之后继续使用它,就会出现悬空指针的问题。这些问题都需要程序员自己来解决,因此在 C++ 中需要更加谨慎地管理内存。

需要注意的是,C++ 标准库中也提供了一些智能指针 (如 unique_ptrshared_ptr 等),它们可以帮助程序员更加方便地管理内存,避免出现内存泄漏和悬空指针等问题。这些智能指针使用了 RAII(资源获取即初始化)的技术,可以在对象生命周期结束时自动释放内存。

类的成员变量和成员函数

成员变量大都以m_开头,这是约定成俗的写法

m_开头既可以一眼看出这是成员变量,又可以和成员函数中的形参名字区分开

cpp 复制代码
class Student{
public:
    // 成员变量
    char *m_name;
    int m_age;
    float m_score;

    // 成员函数
    void say(){
        cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
    }
};

成员函数的定义建议放在类体外面,但成员函数的声明必须在类体内部:

cpp 复制代码
class Student{
public:
    // 成员变量
    char *m_name;
    int m_age;
    float m_score;

    // 成员函数
    void say();  // 对类的成员函数进行声明
};

// 对类的成员函数进行定义
void Student::say(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}

为什么建议在类体外定义成员函数

  1. 使类的声明更加简洁:将成员函数的定义放在类体外,可以使类的声明更加简洁,易于阅读和理解。这样,类的用户可以更加清晰地了解类的接口和实现
  2. 成员函数维护方便:将成员函数的定义放在类体外,可以使代码更加模块化,易于维护和修改。这样,如果需要修改成员函数的实现,只需要修改定义的代码,而不需要修改类的声明
  3. 函数变更后编译速度更快:将成员函数的定义放在类体外,可以减少编译器的工作量,从而加快编译速度。如果将成员函数的定义放在类体内,每次修改成员函数的实现都会导致整个类的重新编译,这会降低编译速度
  4. 避免对象重复定义成员函数:将成员函数的定义放在类体外,可以避免在多个对象中重复定义成员函数的问题。减少内存消耗

成员函数的重载

在 C++ 中,成员函数的重载指的是在同一个类中定义多个具有相同名称但参数列表不同的成员函数。通过成员函数的重载,我们可以为同一个类定义多个不同的行为,以适应不同的使用场景

cpp 复制代码
#include <iostream>

class MyClass {
public:
    int add(int a, int b);
    double add(double a, double b);
};

int MyClass::add(int a, int b) {
    return a + b;
}
double MyClass::add(double a, double b) {
    return a + b;
}

int main() {
    MyClass obj;
    int result1 = obj.add(1, 2);
    double result2 = obj.add(1.5, 2.5);
    std::cout << "result1 = " << result1 << std::endl;
    std::cout << "result2 = " << result2 << std::endl;
    return 0;
}

MyClass 类定义了两个名为 add 的成员函数。第一个成员函数接受两个整数作为参数,返回它们的和;第二个成员函数接受两个浮点数作为参数,返回它们的和。由于这两个成员函数的参数列表不同,因此它们可以被区分开来,从而实现了成员函数的重载。

在使用成员函数时,编译器 会根据传递的参数类型和个数 来选择合适的成员函数进行调用

类成员的访问权限

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

  • 在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制
  • 在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员

student.h 文件 ------ Student 类的声明

cpp 复制代码
#ifndef STUDENT_H
#define STUDENT_H

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();
};

#endif

student.cpp 文件 ------ Student 类的成员函数的定义

cpp 复制代码
#include <iostream>
#include "Student.h"
using namespace std;

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;
}

main.cpp 文件 ------ 写 main 函数

cpp 复制代码
#include <iostream>
#include "Student.h"
using namespace std;

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();
    delete pstu;

    return 0;
}
cpp 复制代码
运行结果:  
小明的年龄是15,成绩是92.5  
李华的年龄是16,成绩是96

上面例子中:

  1. 在实际开发中,我们通常将类的声明放在头文件 中,而将成员函数的定义放在源文件
  2. 成员变量大都以m_开头 :以m_开头既可以一眼看出这是成员变量,又可以和成员函数中的形参名字区分开。以 setname() 为例,如果将成员变量m_name的名字修改为name,那么 setname() 的形参就不能再叫name了,得换成诸如name1_name这样没有明显含义的名字,否则name=name;这样的语句就是给形参name赋值,而不是给成员变量name赋值

类的构造函数(new 时执行)

在上面例子中为了给对象 pstu 里的三个变量赋值,必须得分别调用三个成员函数给它们赋值,比较麻烦,于是引入特殊的成员函数------构造函数可以简化赋值过程:

cpp 复制代码
#include <iostream>
using namespace std;

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    // 声明构造函数
    Student(char *name, int age, float score);
    // 声明普通成员函数
    void show();
};

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

int main(){
    // 1. 创建对象时向构造函数传参
    Student stu("小明", 15, 92.5f);
    stu.show();
    // 2. 创建对象时向构造函数传参
    Student *pstu = new Student("李华", 16, 96);
    pstu -> show();

    return 0;
}

需要注意:

  • 构造函数的函数名必须和类名相同
  • 构造函数必须是 public 属性的
  • 构造函数不需要有返回值

Student 类有了构造函数 Student 之后可以使用 Student *pstu = new Student("李华", 16, 96); 一行代码完成对 pstu 对象的成员变量赋值

构造函数的重载

cpp 复制代码
#include <iostream>
using namespace std;

class Student{
private:
    char *m_name;
    int m_age;
    float m_score;
public:
    Student();
    Student(char *name, int age, float score);
    void show();
};

Student::Student(){
    m_name = NULL;
    m_age = 0;
    m_score = 0.0;
}
Student::Student(char *name, int age, float score){
    m_name = name;
    m_age = age;
    m_score = score;
}
void Student::show(){
    if(m_name == NULL || m_age <= 0){
        cout<<"成员变量还未初始化"<<endl;
    }else{
        cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
    }
}

int main(){
    // 调用构造函数 Student(char *, int, float)
    Student *pstu1 = new Student("小明", 15, 92.5f);
    pstu1 -> show();

    // 调用构造函数 Student()
    Student *pstu2 = new Student();
    pstu2 -> show();

    return 0;
}
小明的年龄是15,成绩是92.5
成员变量还未初始化

构造函数Student(char *, int, float)为各个成员变量赋值,构造函数Student()将各个成员变量的值设置为空,它们是重载关系

实际上,每个类都有默认的构造函数,就如上面例子的Student()函数,没有任何参数,只是单纯创建对象,所以还需要调用其他成员函数进行赋值。

初始化 const 变量

初始化 const 变量必须采用初始化列表的方式:

cpp 复制代码
class VLA{
private:
    const int m_len;
    int *m_arr;
public:
    VLA(int len);
};

//必须使用初始化列表来初始化 m_len
VLA::VLA(int len): m_len(len){
    m_arr = new int[len];
}

由于成员变量 len 是 const 类型的,所以必须通过初始化列表 m_len(len) 的方式对 len 变量进行初始化

类的析构函数(delete 时执行)

析构函数的名字是在类名前面加一个~符号

cpp 复制代码
#include <iostream>
using namespace std;

class VLA{
public:
    VLA(int len);  // 构造函数
    ~VLA();  // 析构函数
public:
    void input();  // 从控制台输入数组元素
    void show();  // 显示数组元素
private:
    int *at(int i);  // 获取第i个元素的指针
private:
    const int m_len;  // 数组长度
    int *m_arr; // 数组指针
    int *m_p;  // 指向数组第i个元素的指针
};

VLA::VLA(int len): m_len(len){  // 使用初始化列表来给 m_len 赋值
    if(len > 0) {
        m_arr = new int[len];  /*分配内存*/
    } else {
        m_arr = NULL;
    }
}
VLA::~VLA(){
    delete[] m_arr;  // 释放内存
}
void VLA::input(){
    for(int i=0; m_p=at(i); i++){ cin>>*at(i); }
}
void VLA::show(){
    for(int i=0; m_p=at(i); i++){
        if(i == m_len - 1){ cout<<*at(i)<<endl; }
        else{ cout<<*at(i)<<", "; }
    }
}
int * VLA::at(int i){
    if(!m_arr || i<0 || i>=m_len){ return NULL; }
    else{ return m_arr + i; }
}

int main(){
    // 创建一个有n个元素的数组(对象)
    int n;
    cout<<"Input array length: ";
    cin>>n;
    VLA *parr = new VLA(n);
    cout<<"Input "<<n<<" numbers: ";
    parr -> input(); // 输入数组元素
    cout<<"Elements: ";
    parr -> show(); // 输出数组元素
    delete parr; // 删除数组(对象),此时析构函数执行

    return 0;
}

析构函数的执行时机

析构函数在对象被销毁时调用,而对象的销毁时机与它所在的内存区域有关

两种不同创建对象的方式:

  • 通过类创建的对象这个变量是在栈内存
  • 通过 new 关键字创建的对象这个变量是在堆内存

搜索

  • 在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数
  • new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

class Demo{
public:
    Demo(string s);
    ~Demo();
private:
    string m_s;
};
Demo::Demo(string s): m_s(s){ }
Demo::~Demo(){ cout<<m_s<<endl; }

void func(){
    Demo obj1("1"); // 局部对象,func 函数执行完就销毁
    cout<<"func 函数执行完成"<<endl;
}

Demo obj2("2"); // 全局对象

int main(){
    Demo obj3("3"); // 局部对象,main 函数执行结束销毁
    Demo *pobj4 = new Demo("4"); // new 创建的对象,被 delete 掉才会销毁
    func();
    cout<<"main 函数执行完成"<<endl;
  
    return 0;
}

运行结果:

css 复制代码
func 函数执行完成
1
main 函数执行完成
3
2

由于对象指针 pobj4 没有被 delete 掉所以没有打印出 4

this 指针(指向对象)

  • this 是 C++ 中的一个 const 指针其值不能被修改
  • this 只在成员函数内部 使用,通过 this 可以访问类的所有成员 ,包括 private、protected、public 属性的,得用->来访问成员变量或成员函数
  • this 指向当前对象,通过它可以访问当前正在使用的对象的所有成员
  • 只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用
cpp 复制代码
#include <iostream>
using namespace std;

class Student{
public:
    void setname(char *name);
    void setage(int age);
    void setscore(float score);
    void show();
private:
    char *name;
    int age;
    float score;
};

void Student::setname(char *name){
    this->name = name;
}
void Student::setage(int age){
    this->age = age;
}
void Student::setscore(float score){
    this->score = score;
}
void Student::show(){
    cout<<this->name<<"的年龄是"<<this->age<<",成绩是"<<this->score<<endl;
}

int main(){
    Student *pstu = new Student;
    pstu -> setname("李华");
    pstu -> setage(16);
    pstu -> setscore(96.5);
    pstu -> show();

    return 0;
}

运行结果

李华的年龄是16,成绩是96.5

this 的值等于对象指针

this 虽然用在类的内部,但是只有在对象被创建以后才会给 this 赋值,并且这个赋值的过程是编译器自动完成的。上面例子中,this 的值和 pstu 的值是相同的

我们不妨来证明一下,给 Student 类添加一个成员函数 printThis(),专门用来输出 this 的值,如下所示:

cpp 复制代码
void Student::printThis(){
    cout<<this<<endl;
}

然后在 main() 函数中创建对象并调用 printThis():

cpp 复制代码
Student *pstu1 = new Student;
pstu1 -> printThis();
cout<<pstu1<<endl;

Student *pstu2 = new Student;
pstu2 -> printThis();
cout<<pstu2<<endl;

运行结果:

0x7b17d8  
0x7b17d8  
0x7b17f0  
0x7b17f0

可以发现,this 确实指向了当前对象,而且对于不同的对象,this 的值也不一样

this 的本质

this 本质上是成员函数的一个形参 ,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中

成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁

static 静态成员变量(数据共享)

对象的内存中包含了成员变量,不同的对象占用不同的内存(实例化),这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响

可是有时候我们希望在多个对象之间共享数据 ,对象 a 改变了某份数据后对象 b 可以检测到 。于是引入静态成员变量来实现。静态成员变量是一种特殊的成员变量 ,它被关键字static修饰

静态成员变量如何实现数据共享

静态成员变量如何实现数据共享?

static 静态成员变量属于类,不属于某个具体的对象 ,即使创建多个对象,也只为静态成员变量分配一份内存,所有对象使用的都是这份内存中的数据。当某个对象修改了这个静态成员变量,也会影响到其他对象

以前面的 Student 类为例,如果我们想知道班级中共有多少名学生,就可以设置一份共享的变量,每次创建对象时让该变量加 1

cpp 复制代码
class Student{
public:
    Student(char *name, int age, float score);
    void show();
public:
    static int m_total;  // 静态成员变量
private:
    char *m_name;
    int m_age;
    float m_score;
};

这段代码声明了一个静态成员变量 m_total,用来统计学生的人数

静态成员变量必须初始化

static 成员变量必须在类声明的外部初始化,具体形式为:

cpp 复制代码
type class::name = value;

type 是变量的类型,class 是类名,name 是变量名,value 是初始值。将上面的 m_total 初始化:

cpp 复制代码
int Student::m_total = 0;

初始化时 才为此静态成员变量分配内存 (不是类声明时,也不是创建对象时),static 成员变量不占用对象的内存,而是在所有对象之外【在内存分区中的全局数据区 】开辟内存,没有初始化的静态成员变量不能被使用

因此,静态成员变量的内存是到程序结束时才释放,跟对象的创建和销毁没有任何关系(跟全局变量一个道理)

静态成员变量在初始化时不能再加 static但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化

访问静态成员变量

cpp 复制代码
// 通过类类访问 static 成员变量,不创建对象也可以先访问
Student::m_total = 10;
// 通过对象来访问 static 成员变量
Student stu("小明", 15, 92.5f);
stu.m_total = 20;
// 通过对象指针来访问 static 成员变量
Student *pstu = new Student("李华", 16, 96);
pstu -> m_total = 20;

静态成员变量使用

cpp 复制代码
#include <iostream>
using namespace std;

class Student{
public:
    Student(char *name, int age, float score);
    void show();
private:
    static int m_total;  // 1. 声明静态成员变量
private:
    char *m_name;
    int m_age;
    float m_score;
};

// 2. 初始化静态成员变量
int Student::m_total = 0;

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
    m_total++;  // 3. 操作静态成员变量
}
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"(当前共有"<<m_total<<"名学生)"<<endl;
}

int main(){
    (new Student("小明", 15, 90)) -> show();
    (new Student("李磊", 16, 80)) -> show();
    (new Student("张华", 16, 99)) -> show();
    (new Student("王康", 14, 60)) -> show();

    return 0;
}

运行结果:

小明的年龄是15,成绩是90(当前共有1名学生)  
李磊的年龄是16,成绩是80(当前共有2名学生)  
张华的年龄是16,成绩是99(当前共有3名学生)  
王康的年龄是14,成绩是60(当前共有4名学生)

本例中将 m_total 声明为静态成员变量,每次创建对象时 ,会调用构造函数使 m_total 的值加 1

静态成员函数

静态成员函数属于类不属于 任意一个对象,所以不需要 this 指针 ,因此无法访问普通成员变量,只能访问静态成员变量

下面是一个完整的例子,该例通过静态成员函数来获得学生的总人数和总成绩:

cpp 复制代码
#include <iostream>
using namespace std;

class Student{
public:
    Student(char *name, int age, float score);
    void show();
public:  // 1. 声明静态成员函数
    static int getTotal();
    static float getPoints();
private:
    static int m_total;  // 总人数
    static float m_points;  // 总成绩
private:
    char *m_name;
    int m_age;
    float m_score;
};

int Student::m_total = 0;
float Student::m_points = 0.0;

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){
    m_total++;
    m_points += score;
}
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
// 2. 定义静态成员函数
int Student::getTotal(){
    return m_total;
}
float Student::getPoints(){
    return m_points;
}

int main(){
    (new Student("小明", 15, 90.6)) -> show();
    (new Student("李磊", 16, 80.5)) -> show();
    (new Student("张华", 16, 99.0)) -> show();
    (new Student("王康", 14, 60.8)) -> show();

    int total = Student::getTotal(); // 3. 使用静态成员函数读取静态成员变量
    float points = Student::getPoints();
    cout<<"当前共有"<<total<<"名学生,总成绩是"<<points<<",平均分是"<<points/total<<endl;

    return 0;
}

运行结果:

小明的年龄是15,成绩是90.6  
李磊的年龄是16,成绩是80.5  
张华的年龄是16,成绩是99  
王康的年龄是14,成绩是60.8  
当前共有4名学生,总成绩是330.9,平均分是82.725

此例中,总人数 m_total 和总成绩 m_points 由各个对象累加得到,必须声明为 static 才能共享;getTotal()、getPoints() 分别用来获取总人数和总成绩,为了访问 static 成员变量,我们将这两个函数也声明为 static

const 成员变量和成员函数

如果不希望成员变量和成员函数被修改,可以使用const关键字加以限定

const 成员变量

const 成员变量的用法和普通 const 变量的用法相似,只需要在声明时加上 const 关键字。初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表

const 成员函数(常成员函数)

const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,这种措施主要还是为了保护数据而设置的。const 成员函数也称为常成员函数。

我们通常将 get 函数设置为常成员函数。读取成员变量的函数的名字通常以get开头,后跟成员变量的名字,所以通常将它们称为 get 函数。

常成员函数需要在声明和定义的时候在函数头部的结尾(位置需要重点注意)加上 const 关键字,请看下面的例子:

cpp 复制代码
class Student{
public:
    Student(char *name, int age, float score);
    void show();
    // 声明 常成员函数
    char *getname() const;
    int getage() const;
    float getscore() const;
private:
    char *m_name;
    int m_age;
    float m_score;
};

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
void Student::show(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<endl;
}
//定义常成员函数
char * Student::getname() const{
    return m_name;
}
int Student::getage() const{
    return m_age;
}
float Student::getscore() const{
    return m_score;
}

需要注意:必须在成员函数的声明和定义处同时加上 const 关键字

区分一下 const 的位置:

  • 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()
  • 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const

const 对象(常对象)

const 也可以用来修饰对象,称为常对象 。一旦将对象定义为常对象之后,就只能调用类的 const 成员(包括 const 成员变量和 const 成员函数)了

cpp 复制代码
const Student stu("小明", 15, 90.6);
const Student *pstu = new Student("李磊", 16, 80.5);

友元函数和友元类

友元函数

一个类 A 主动把一个外部函数 Fun(全局函数或另一个类 B 的成员函数)用关键字 friend 标记为友元函数 ,就表示主动允许 外部函数 Fun 读取类 A 的 private 成员了

从本质上看,就是把对象指针 传进友元函数,才能通过友元函数去读取类的 private 成员

1. 全局范围内的友元函数

目标:

  1. 声明一个 Student 类和一个全局函数 show
  2. 实现:全局函数 show 能读取 Student 类里的 private 成员变量 m_name、m_age、m_score
cpp 复制代码
#include <iostream>
using namespace std;

class Student{
public:
    Student(char *name, int age, float score);
public:
    friend void show(Student *pstu);  // 1. 将show() 声明为友元函数
private:
    char *m_name;
    int m_age;
    float m_score;
};

Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }

// 2. 全局函数 show,形参为对象指针,通过对象指针读取 Student 类成员
void show(Student *pstu){
    cout<<pstu->m_name<<"的年龄是 "<<pstu->m_age<<",成绩是 "<<pstu->m_score<<endl;
}

int main(){
    Student stu("小明", 15, 90.6);
    show(&stu);  // 3. 把对象指针 &stu 传进去调用友元函数
    Student *pstu = new Student("李磊", 16, 80.5);
    show(pstu);  // 3. 把对象指针 pstu 传进去调用友元函数

    return 0;
}

友元函数是不同于类的成员函数,在友元函数中不能直接访问 类的成员,必须要借助对象 ,所以以上例子把 &stu 和 pstu 对象指针传进去,友元函数 show() 设置形参也为对象指针,通过对象指针才能去访问 Student 类里的成员变量和成员函数

2. 其他类的成员函数声明为友元函数

目标:

  1. 声明两个类:Student 类和 Address 类
  2. 实现:Student 类的成员函数 show 能读取 Address 类的 private 成员变量 m_province、m_city、m_district
cpp 复制代码
#include <iostream>
using namespace std;

class Address;  // 提前声明 Address 类(先指定这个类是存在的)

// 声明 Student 类
class Student{
public:
    Student(char *name, int age, float score);
public:
    void show(Address *addr); // 1. Student 类写一个友元函数 show(),形参是 Address 类的对象指针
private:
    char *m_name;
    int m_age;
    float m_score;
};

// 声明 Address 类(先指定这个类包含这些成员)
class Address{
private:
    char *m_province;  // 省份
    char *m_city;  // 城市
    char *m_district;  // 区(市区)
public:
    Address(char *province, char *city, char *district);
    // 2. 将 Student 类中的成员函数 show() 声明为友元函数
    friend void Student::show(Address *addr);
};

// 定义 Student 类的构造函数
Student::Student(char *name, int age, float score): m_name(name), m_age(age), m_score(score){ }
// 3. 定义 Student 类的 show 成员函数,接收形参 *addr 对象指针,可以读取 Address 类的成员变量
void Student::show(Address *addr){
    cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
    cout<<"家庭住址:"<<addr->m_province<<"省"<<addr->m_city<<"市"<<addr->m_district<<"区"<<endl;
}

// 定义 Address 类的构造函数
Address::Address(char *province, char *city, char *district){
    m_province = province;
    m_city = city;
    m_district = district;
}

int main(){
    Student stu("小明", 16, 95.5f);
    Address addr("陕西", "西安", "雁塔");
    stu.show(&addr); // 4. 把对象指针 &addr 传入 stu 对象的成员函数 show 里面
   
    Student *pstu = new Student("李磊", 16, 80.5);
    Address *paddr = new Address("河北", "衡水", "桃城");
    pstu -> show(paddr); // 4. 把对象指针 paddr 传入 pstu 指向的对象的成员函数 show 里面

    return 0;
}

运行结果:

小明的年龄是 16,成绩是 95.5  
家庭住址:陕西省西安市雁塔区  
李磊的年龄是 16,成绩是 80.5  
家庭住址:河北省衡水市桃城区

友元类

一个类 A 主动把另一个类 B 用关键字 friend 标记为友元类 ,就表示主动允许读取 类 A 的 private 成员了

除非有必要,一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些

使用上只需要修改 Address 类即可,其余代码相同

cpp 复制代码
// 声明 Address 类
class Address{
public:
    Address(char *province, char *city, char *district);
public:
    // 将Student类声明为Address类的友元类
    friend class Student;
private:
    char *m_province;  //省份
    char *m_city;  //城市
    char *m_district;  //区(市区)
};

关于友元,有两点需要说明:

  • 友元的关系是单向的而不是双向的。如果声明了类 B 是类 A 的友元类,不等于类 A 是类 B 的友元类,类 A 中的成员函数不能访问类 B 中的 private 成员。
  • 友元的关系不能传递。如果类 B 是类 A 的友元类,类 C 是类 B 的友元类,不等于类 C 是类 A 的友元类。

String 类字符串

String 类字符串完全可以代替C语言中的字符数组或字符串指针

使用 string 类需要包含头文件<string>,下面的例子介绍了几种定义 string 变量(对象)的方法:

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1; // 默认值空字符串 ""
    string s2 = "c plus plus"; // "c plus plus"
    string s3 = s2; // "c plus plus"
    string s4 (5, 's'); // "sssss"
    return 0;
}

取字符串长度

采用 length() 方法

cpp 复制代码
string s = "http://c.biancheng.net";
int len = s.length();
cout<<len<<endl; // 22

转换为 C 风格的字符串

为了使用C语言中的 fopen() 函数打开文件,必须将 string 字符串转换为C风格的字符串:采用转换函数 c_str()

cpp 复制代码
string path = "D:\demo.txt";
FILE *fp = fopen(path.c_str(), "rt"); // "D:\demo.txt"

读写其中的字符

cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main(){
    string s = "1234567890";
    for(int i=0,len=s.length(); i<len; i++){
        cout<<s[i]<<" "; // 通过下标读取
    }
    cout<<endl;
    s[5] = '5'; // 通过下标直接改写
    cout<<s<<endl;
    return 0;
}

插入字符串

insert (pos, str) 方法

  • pos 表示要插入的位置,也就是下标
  • str 表示要插入的字符串,它可以是 string 字符串,也可以是C风格的字符串
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1, s2, s3;
    s1 = s2 = "1234567890";
    s3 = "aaa";
    s1.insert(5, s3);
    cout<< s1 <<endl;
    s2.insert(5, "bbb");
    cout<< s2 <<endl;
    return 0;
}

运行结果:

12345aaa67890  
12345bbb67890

删除子字符串

erase (pos = 0, len = npos);

  • pos 表示要删除的子字符串的起始下标,默认值是 0
  • len 表示要删除子字符串的长度。len 不填表示直接删除从 pos 到字符串结束处的所有字符
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1, s2, s3;
    s1 = s2 = s3 = "1234567890";
    s2.erase(5);
    s3.erase(5, 3);
    cout<< s1 <<endl;
    cout<< s2 <<endl;
    cout<< s3 <<endl;
    return 0;
}

运行结果:

1234567890  
12345  
1234590

提取子字符串

str = substr (pos = 0, len = npos) const;

  • str 为提取后返回的子字符串
  • pos 为要提取的子字符串的起始下标
  • len 为要提取的子字符串的长度,len 不填表示提取从 pos 到字符串结束处的所有字符
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1 = "first second third";
    string s2;
    s2 = s1.substr(6, 6);
    cout<< s1 <<endl;
    cout<< s2 <<endl;
    return 0;
}

运行结果:

sql 复制代码
first second third  
second

替换子字符串

  • 方法一:先用 erase 方法删除子字符串后用 insert 方法插入新子字符串
  • 方法二:提取前半部分子字符串后拼接新子字符串和后半部分子字符串完成替换
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1, s2;
    s1 = s2 = "1234567890";
    s1.erase(5, 3);
    s1.insert(5, "bbb");
    s2 = s2.substr(0, 5) + "bbb" + s2.substr(8);
    cout<< s1 <<endl;
    cout<< s2 <<endl;
    return 0;
}

运行结果:

12345bbb90
12345bbb90

查找子字符串

index = big_str.find (small_str, pos = 0)

  • index 为查找到的子字符串的开始的下标,找不到则返回 -1
  • big_str 为被遍历的的大字符串
  • small_str 为要查找的子字符串
  • pos 为开始查找的 big_str 的下标,不填默认为从 0 开始
cpp 复制代码
#include <iostream>
#include <string>
using namespace std;

int main(){
    string s1 = "first second third";
    string s2 = "second";
    string s3 = "asecond";
    int index1 = s1.find(s2,5);
    int index2 = s1.find(s3,5);
    cout<<"index1 : "<< index1 <<endl;
    cout<<"index2 : "<< index2 <<endl;
    return 0;
}

运行结果:

diff 复制代码
index1 : 6
index2 : -1

class 和 struct 的区别

C++中的 struct 和 class 基本是通用的,唯有几个细节不同:

  • 使用 class 时,类中的成员默认都是 private 属性 的;而使用 struct 时,结构体中的成员默认都是 public 属性
  • class 继承默认是 private 继承 ,而 struct 继承默认是 public 继承
  • class 可以使用模板,而 struct 不能

参考内容

内容主要来源于 C++入门教程,C++基础教程,进行归纳总结梳理

相关推荐
zfenggo12 分钟前
c/c++ 无法跳转定义
c语言·开发语言·c++
图灵猿15 分钟前
【Lua之·Lua与C/C++交互·Lua CAPI访问栈操作】
c语言·c++·lua
A懿轩A42 分钟前
C/C++ 数据结构与算法【树和二叉树】 树和二叉树,二叉树先中后序遍历详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·二叉树·
hjxxlsx1 小时前
探索 C++ 自定义函数的深度与广度
开发语言·c++
lijiachang0307182 小时前
设计模式(一):单例模式
c++·笔记·学习·程序人生·单例模式·设计模式·大学生
<但凡.2 小时前
题海拾贝:蓝桥杯 2020 省AB 乘法表
c++·算法·蓝桥杯
DogDaoDao3 小时前
leetcode 面试经典 150 题:矩阵置零
数据结构·c++·leetcode·面试·矩阵·二维数组·矩阵置零
计科土狗4 小时前
前缀和与差分
c++·算法
午言若5 小时前
MYSQL 架构
c++·mysql
羑悻的小杀马特7 小时前
【AIGC篇】畅谈游戏开发设计中AIGC所发挥的不可或缺的作用
c++·人工智能·aigc·游戏开发