跳转至

C++类和对象

约 2295 个字 424 行代码 2 张图片 预计阅读时间 13 分钟

类与对象的基本认识

C语言中的面向过程指只要涉及到解决问题的主要步骤就需要去实现对应的方法,不论这个方法属于哪一个对象的,而在C++中,每一个对象只需要实现自己的方法,剩下的交给对象之间的交互完成即可

类和对象的基本概念

类的认识

在C语言中,结构体中只能定义变量,但是在C++中,结构体可以定义变量和函数

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//C语言的单链表
typedef struct SList
{
    int val;
    struct SList* node;
}SList;
//单链表的实现方法
//...

//C++中的单链表
//C++中定义结构体可以省略typedef,创建结构体变量也可以不需要写struct
struct SList
{
    int val;
    SList* node;//C++中的结构体属于类的一种,所以可以直接使用类名创建变量
    //单链表的实现方法
    //...

};

但是在C++中,更喜欢用class代表类,struct更多还是结构体,所以上面代码改为

C++
1
2
3
4
5
6
7
8
//C++中的单链表
class SList
{
    int val;
    SList* node;
    //单链表的实现方法
    //...
}

类的定义

在C++中,使用下面的语法进行类的定义

C++
1
2
3
4
5
6
7
class className
{
    //成员变量

    //实现方法

};//注意不要遗忘分号

在类的定义语法中,class为类定义的关键字,className为类名,{}中的内容为类的主体

类的内容一般称作成员,而类中的变量一般成为成员变量,类中的方法一般称为方法或者成员函数

Note

需要注意的是,成员变量可以出现在成员函数的上方也可以出现在成员函数的下方,因为编译器在寻找成员变量时是将类当做一个整体看待,此时不论是在成员函数的上方还是下方都能寻找到需要的成员变量

类的两种定义方式

类的声明和定义分开

在C++中,如果在大项目中,一般使用类的声明和定义分开的方式,即在头文件中声明,在实现文件中定义,但是需要注意下面的问题:

  1. 因为声明和定义分开处理,所以定义所在的文件需要引入类声明所在的头文件
  2. 当定义需要实现类中声明的方法时将看不到放在头文件中的类的成员变量,此时需要使用域作用限定符指定对应的类

代码实例:

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
//class_test.h
#pragma once

class Person
{
    //实现方法
    int test();

    //成员变量
    int age = 0;
    int num = 0;
};

//class_test.cpp(方法定义文件)
#include "class_test.h"//需要包含类所在的头文件

int Person::test()//使用域作用限定符指定是哪个类中的方法
{
    return age + num;
}
类的声明和定义放在一起

当项目不够大时,或者只是简单的实现时,可以考虑将类的声明和定义放在同一个文件中,需要注意的是,当成员函数的声明和定义在一起时,编译器可能会当做内联函数处理

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Person1
{
    //实现方法
    int test()
    {
        return age + num;
    }

    //成员变量
    int age;
    int num;
};

成员变量名的命名规则建议

当成员函数的定义在类中时,需要考虑到成员函数的形式参数(局部变量)和成员变量之间的关系

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//成员变量命名
class test
{
    int num1;
    int num2;
public:
    int add(int num1, int num2)
    {
        num1 = num1;
        num2 = num2;
        return num1 + num2;
    }
};

#include <iostream>
using namespace std;

int main()
{
    test t;
    cout << t.add(1, 2) << endl;

    return 0;
}
输出结果
3

如果像上面的代码中,成员变量和add函数中的局部变量相同,那么会因为局部优先原则导致成员变量并没有被赋值成功,如下图所示

而如果将成员变量和add函数中的局部变量加以区分,则此时可以成功将局部变量的值存入成员变量中

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class test
{
    int _num1;
    int _num2;
public:
    int add(int num1, int num2)
    {
        _num1 = num1;
        _num2 = num2;
        return num1 + num2;
    }
};

#include <iostream>
using namespace std;

int main()
{
    test t;
    cout << t.add(1, 2) << endl;

    return 0;
}
输出结果
3

结果如下图所示:

Note

上面的区分成员变量和形式参数的方式仅供参考,具体以环境要求为主

类的封装

面向对象的三大特性:封装、继承和多态

在C++中,类的封装表示将方法和成员变量进行结合,对外只通过公开的接口与其他对象进行交互

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

C++
1
2
3
4
5
6
7
8
9
class Person
{
    //实现方法
    int test();

    //成员变量
    int age = 0;
    int num = 0;
};

在上面的代码中,test函数并没有公开,因为访问权限的问题,所以外部对象将无法使用

类的访问限定符

在C++中,一共有三个访问限定关键字,即private(私有)、protected(受保护)以及public(公开)

在C++中,类的访问权限有下面的特点

  1. public修饰的成员在类外可以直接被访问
  2. protectedprivate修饰的成员在类外不能直接被访问
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到}即类结束
  4. class的默认访问权限为privatestructpublic(因为struct要兼容C语言)
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//结构体默认是public,而类默认是private
#include <iostream>
using namespace std;

struct test
{
    int val;
    int test1()
    {
        cout << "结构体test1函数" << endl;
        return val;
    }
};

class test2
{
    int val1;
    int test3()
    {
        cout << "类test3函数" << endl;
        return val1;
    }
};

int main()
{
    test t1; //结构体变量
    t1.val = 0;//可以直接访问成员变量
    t1.test1();//可以直接访问成员函数
    test2 t2;
    t2.val1 = 0;//无法直接访问,因为是private修饰的变量
    t2.test3();//无法直接访问,因为是private修饰的函数
    return 0;
}
报错信息
test2::val1: 无法访问 private 成员(test2类中声明)    
test2::test3: 无法访问 private 成员(test2类中声明)

需要访问类中的函数时,可以指定访问权限,使用public后可以在类外访问

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
using namespace std;

struct test
{
    int val;
    int test1()
    {
        cout << "结构体test1函数" << endl;
        return val;
    }
};

class test2
{
public:
    int val1;
    int test3()
    {
        cout << "类test3函数" << endl;
        return val1;
    }
};

int main()
{
    test t1; //结构体变量
    t1.val = 0;//可以直接访问成员变量
    t1.test1();//可以直接访问成员函数
    test2 t2;
    t2.val1 = 0;//无法直接访问,因为是private修饰的变量
    t2.test3();//无法直接访问,因为是private修饰的函数
    return 0;
}
输出结果
结构体test1函数
类test3函数
#include <iostream>
using namespace std;

class test4
{
    //private修饰val2,到public结束
private:
    int val2 = 1;
    //public修饰test5,到右大括号结束
public:
    int test5()
    {
        cout << "类test4函数" << endl;
        return val2;
    }
};

int main()
{
    test4 t;
    t.val2;//无法访问,因为是private修饰
    t.test5();//可以访问,因为是public修饰

    return 0;
}
错误信息
test4::val2: 无法访问 private 成员(test4类中声明)

类的作用域

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

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
//class_test.h
#pragma once

class Person
{
    //实现方法
    int test();

    //成员变量
    int age = 0;
    int num = 0;
};

//class_test.cpp(方法定义文件)
#include "class_test.h"

int Person::test()//使用域作用限定符指定是哪个类中的方法
{    
    //之所以能访问到私有变量age和num是因为指定了test函数为Person类中的方法
    //一个类中可以访问到private变量
    return age + num;
}

对象

类的实例化

用类类型创建对象的过程,称为类的实例化

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

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //无法直接使用类名访问成员,除非成员是静态的
    #include <iostream>
    using namespace std;
    
    class Person
    {
    public:
        int age;
        static int num;
    };
    
    int main()
    {
        Person::age = 0;//类在没有对象之前不可以直接使用非静态的成员变量
        Person::num = 0;//静态变量可以直接用类名访问
        return 0;
    }
    报错信息
    对非静态成员Person::age的非法引用
    

    在上面的代码中,因为类中的变量只是声明,告诉编译器当前类有哪些成员变量,但是并不为其开辟空间,而静态变量在编译过程中会分配空间,故可以直接用类名进行访问

    需要访问类中的非静态变量时,需要用类名实例化出对象,再用对象去访问类中的变量

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <iostream>
    using namespace std;
    
    class Person
    {
    public:
        int age;
    };
    
    int main()
    {
        Person p;//实例化,创建对象
        p.age = 10;
        cout << p.age << endl;
        return 0;
    }
    输出结果
    10
    

    在上面的代码中,因为pPerson类的对象,故在编译时会为p分配空间,此时p对象的成员变量age就有了存储空间

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

    C++
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <iostream>
    using namespace std;
    
    class Person
    {
    public:
        int age;
    };
    
    int main()
    {
        //Person类的三个对象
        Person p;
        Person p1;
        Person p2;
        p.age = 10;
        p1.age = 20;
        p2.age = 30;
        cout << p.age << endl;
        cout << p1.age << endl;
        cout << p2.age << endl;
        return 0;
    }
    输出结果
    10
    20
    30
    

类对象的大小

在计算类对象的大小时,和计算结构体大小的方式相同,并且都需要考虑到内存对齐的规则,成员函数和静态成员均不会考虑到大小计算中

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Person
{
    int age;
    int num;

    int test()
    {
        cout << "Person类的函数 " << age << " " << num << " " << endl;
    }
};

int main()
{
    cout << sizeof(Person) << endl;
    return 0;
}
输出结果
8

Quote

计算一个类的大小实际上是在计算类对象的大小,所以有sizeof(类)=sizeof(类对象)

在类对象访问成员变量时,每一个成员变量属于各自的对象存放在栈区,而成员函数是所有对象公共的存放在公共代码段区

如果类中没有成员变量,即只包括成员函数或者是空类时,类的大小为1个字节,这一个字节是为了表示给类占位,标识这个类存在

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Empty
{

};

class NoneMember
{
    void test()
    {
        cout << endl;
    }
};

int main()
{
    cout << sizeof(Empty) << endl;
    cout << sizeof(NoneMember) << endl;

    return 0;
}
输出结果
1
1

this关键字

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Person
{
public:
    int age;
    int num;

    void test()
    {
        cout << "Person类的函数 " << age << " " << num << " " << endl;
    }
};

int main()
{
    Person p1;
    Person p2;
    p1.age = 10;
    p1.num = 10;
    p2.age = 20;
    p2.num = 20;
    p1.test();
    p2.test();
    return 0;
}
输出结果
Person类的函数 10 10
Person类的函数 20 20

在上面的代码中,对象p1和对象p2的成员变量agenum有自己的存储的空间,两个对象的成员变量不会冲突,并且尽管test函数是公共的,在test函数中agenum也对应着各自的对象

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

C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
//函数初始时
void test(this*)
{
    cout << "Person类的函数 " << this->age << " " << this->num << " " << endl;
{
//当p1对象在调用test函数时,此时this指针中存的时p1对象的地址
void test(&p1)
{
    cout << "Person类的函数 " << p1.age << " " << p1.num << " " << endl;
}

//对于对象p2
//当p2对象在调用test函数时,此时this指针中存的时p2对象的地址
void test(&p2)
{
    cout << "Person类的函数 " << p2.age << " " << p2.num << " " << endl;
}

但是上面的代码只是演示,实际过程中不可以显式得将this作为函数的形参

this指针的特点
  1. this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
  2. 只能在成员函数的内部使用
  3. this指针本质上是成员函数的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
  4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
  5. 一般情况下,this指针存储在栈区上或者寄存器ecx(VS编译器做出的优化)中

Note

注意,this指针有时可以为空,有时不可以为空

  1. 当对象调用函数不需要实际进行解引用操作时,this指针可以为空指针
  2. 当对象调用的函数实际进行解引用操作时,this指针不可以为空指针,否则会出现空指针解引用错误
C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class test
{
private:
    int val = 0;

public:
    void test1()
    {
        cout << "测试函数1" << endl;
    }

    void test2()
    {
        cout << "测试函数2 " << val << endl;
    }
};

int main()
{
    test* p = nullptr;
    p->test1();//空指针可以直接调用
    (*p).test1();//尽管手动解引用,但是实际上编译器处理时只在需要显式解引用的地方才会进行解引用操作,故此时依旧不会出错
    p->test2();//空指针调用会在获取val处出现空指针解引用问题

    return 0;
}

在上面的代码中,二者均不会出现编译错误,但是对于调用test2来说,因为test2函数中涉及到访问成员变量的空间,而指针对象p是空指针,没有实际的地址空间,导致成员变量val的空间也不存在,从而出现空指针解引用错误,而之所以访问成员函数不需要解引用是因为成员函数并不在对象所在的空间中,不需要在对象的空间中找函数,而是直接去公共代码段中寻找