C++ 中的类和对象及其相关概念和用法

先用一个例子简单对类和对象有个概念:

// main.cpp
#include <iostream>
#include "myclass.h"

using std::cout;
using std::endl;

int main()
{
    MYSTACK ms(20);
    for(int i = 0;i < ms.length();i++)
        ms.push(i);

    if (ms.isfull())
        cout<<"The stack is full!"<<endl;
    else
        cout<<"The stack is not full!"<<endl;

    ms.display();

    for(int i = 10;i < 15;i++)
        ms.pop();

    if (ms.isempty())
        cout<<"The stack is empty!"<<endl;
    else
        cout<<"The stack is not empty!"<<endl;

    ms.display();

    return 0;
}
// myclass.h
#ifndef MYCLASS_H
#define MYCLASS_H

class MYSTACK
{
public:
    MYSTACK(int s = 100);
    bool isempty();
    bool isfull();
    int length();
    void push(int n);
    int pop();
    void display();
    ~MYSTACK();

private:
    int *head;
    int size;
    int top;
};

#endif // MYCLASS_H
// myclass.cpp
#include <iostream>
#include "myclass.h"

using std::cout;
using std::endl;

MYSTACK::MYSTACK(int s)
{
    size = s;
    head = static_cast<int *>(new int[size]);
    top = 0;
}

bool MYSTACK::isempty()
{
    return top == 0;
}

bool MYSTACK::isfull()
{
    return top == size;
}

int MYSTACK::length()
{
    return size;
}

void MYSTACK::push(int n)
{
    head[top++] = n;
}

int MYSTACK::pop()
{
    return head[--top];
}

void MYSTACK::display()
{
    int i = -1;
    while(i++ != top-1)
        cout<<"The element is "<<head[i]<<endl;
}

MYSTACK::~MYSTACK()
{
    delete []head;
}

结果为:

The stack is full!
The element is 0
The element is 1
The element is 2
The element is 3
The element is 4
The element is 5
The element is 6
The element is 7
The element is 8
The element is 9
The element is 10
The element is 11
The element is 12
The element is 13
The element is 14
The element is 15
The element is 16
The element is 17
The element is 18
The element is 19
The stack is not empty!
The element is 0
The element is 1
The element is 2
The element is 3
The element is 4
The element is 5
The element is 6
The element is 7
The element is 8
The element is 9
The element is 10
The element is 11
The element is 12
The element is 13
The element is 14

上边的程序中用到了几个概念,如类型转换,new/delete,默认参数,构造器,析构器,多文件编程等。这里主要对之前未提到的概念做一个总结。

构造器

定义

上边的例子中,我们构建了一个自定义类 MYSTACK,对应的构造函数就是 MYSTACK(){},因此我们可以得到构造器的定义为:

class classname
{
    classname(argument)
    {
        statement;
    }
}

作用

构造器的作用就是在类对象创建的时候,完成该对象的初始化,并且该过程是自动完成的,不需要显式调用。

规则

  • 在对象创建时自动调用,进行初始化
  • 无返回值,与类同名
  • 可以重载,可以有默认参数
  • 如果没有对构造器进行显式构建,则系统默认为该类分配一个构造器
  • 如果对构造器进行显式构建,则默认构造器消失,以自实现为准

比如我们之前构建的类,就子实现了构造器,并采用了默认参数。

需要注意的是,如果函数声明和函数定义分开书写的话,只在声明处说明即可。

参数初始化列表

一般情况下,构造器中主要是对类对象的数据成员进行初始化。此时数据成员的初始化既可以在构造器的函数体中实现,也可以以参数初始化列表的形式实现。参数初始化列表的形式为:

classname(argument list):data1(init value),data1(init value),...
{
    statement;
}

参数初始化列表中以 : 为分界线,之前的部分为函数头,之后的部分为参数初始化列表,参数之间用逗号 ,分隔,init value 可以是表达式或者是 argument list 中的值,也可以是常量。比如我们可以将之前的构造器改为:

MYSTACK::MYSTACK(int s):head(static_cast<int *>(new int[s])),size(s),top(0){}

这样也是正确的,但是下边的修改可能结果不会报错,但是却是有问题的:

​MYSTACK::MYSTACK(int s):size(s),head(static_cast<int *>(new int[size])),top(0){}

上边的结果会提示:

warning: 'MYSTACK::size' will be initialized after [-Wreorder]

 好像是提示初始化的顺序。再看一个关于参数初始化列表的例子:

#include <iostream>
#include <string>

using namespace std;

class A
{
public:
    A(string ps):name(ps),len(name.size()){}

    void dis()
    {
        cout<<len<<endl;
        cout<<name<<endl;
        cout<<name.size()<<endl;
    }
private:
    int len;
    string name;
};

int main()
{
    A a("Apple");
    a.dis();
    return 0;
}

 结果为:

94
Apple
5

上边的结果却跟我们预想的不太一样,并且出现了和之前相同的警告,这说明:

  • 参数初始化列表只能用于构造器中
  • 初始化列表中的初始化顺序,至于数据成员的声明顺序有关
  • 必须使用此格式来初始化非静态 const 数据成员(c++98)
  • 必须使用此格式来初始化引用数据

析构器

定义

上边的例子中,我们构建了一个自定义类 MYSTACK,对应的析构函数就是 ~MYSTACK(){},因此我们可以得到析构器的定义为:

class classname
{
    ~classname()
    {
        statement;
    }
}

作用

析构器的作用就是在类对象销毁的时候,完成类对象的销毁,尤其是类中申请了堆上内存。

类对象销毁时期

  • 栈对象离开其作用域
  • 堆对象被手动delete

规则

  • 在对象销毁时自动调用,进行类对象的销毁
  • 无返回值,与类同名
  • 没有参数
  • 不可以重载
  • 如果没有对析构器进行显式构建,则系统默认为该类分配一个析构器
  • 如果对析构器进行显式构建,则默认析构器消失,以自实现为准

这里将之前的析构函数修改为下边的形式,简单说明栈对象和堆对象的析构器调用:

MYSTACK::~MYSTACK()
{
    cout<<"~MYSTACK()"<<endl;
    delete []head;
}
// main.cpp
#include <iostream>
#include "myclass.h"

using std::cout;
using std::endl;

int main()
{
    MYSTACK ms;

    MYSTACK *p = new MYSTACK;
    delete p;

    return 0;
}

结果为:

~MYSTACK()
~MYSTACK()

上边的结果显示,堆对象在手动 delete 的时候调用了析构器,栈对象在 main 结束的时候调用了析构器。

拷贝构造

定义

class classname
{
    classname(const classname & var)
    {
        statement;
    }
}

作用

由已经存在的对象,创建新的对象。也就是说该新对象是拷贝旧对象得到的。

拷贝构造发生时期

  • 类对象拷贝,制作副本
  • 以类对象作为参数和返回值

规则

  • 拷贝构造默认为等位拷贝,也就是浅拷贝
  • 深拷贝需要自实现
  • 如果没有对拷贝构造进行显式构建,则系统默认为该类分配一个拷贝构造
  • 如果对拷贝构造进行显式构建,则默认拷贝构造消失,以自实现为准

深拷贝和浅拷贝

深拷贝与浅拷贝的不同主要发生在存在堆上空间的时候,两者之间的关系可以用下图说明:

从上图中我们可以看出,深拷贝和浅拷贝的不同主要分为两种情况:

  • 如果类对象的数据成员没有申请堆上空间的话,深拷贝和浅拷贝是一样的
  • 如果类对象的数据成员申请了堆上空间的话,深拷贝会将源对象的堆上空间也进行拷贝,而浅拷贝只是做了一个链接
#include <iostream>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):name(name),num(num),grade(grade){}

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT(){}

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

结果为:

name is zhangsan
num is 0001
grade is 80
name is zhangsan
num is 0001
grade is 80

从上边的结果可以看出两者的结果是一样的,但是如果选择为 name 成员申请堆上空间就会出现不同的情况:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

使用 g++ 编译运行结果为:

name is zhangsan
num is 100
grade is 80
name is zhangsan
num is 100
grade is 80
*** Error in `./a.out': double free or corruption (fasttop): 0x0000000000c5ec20 ***
...

也就是说在 main 函数中,对于 st 和 sd 的 name 成员指向的堆上空间进行了多次释放。这也就是浅拷贝的不足,可以另外构建深拷贝的拷贝构造函数来避免该问题:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

结果为:

name is zhangsan
num is 100
grade is 80
name is zhangsan
num is 100
grade is 80

但是在修改的时候,发现如果将程序改为:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, char *num, int grade):grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        this->num = new char[strlen(num)+1];
        strcpy(this->num,num);
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
        delete []num;
    }

private:
    char *name;
    char *num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan","100",80);

    st.display();

    STUDENT sd(st);

    sd.display();

    return 0;
}

上边的程序中同样发生了 double free,但是在 Linux 中编译运行的结果却没有显示错误。现在还不知道什么原因。

特性

在发生浅拷贝时,如果源对象数据成员申请的有堆上空间,那么源对象和拷贝对象的该数据都指向同一块堆上空间。

传参

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    ~STUDENT()
    {
        cout<<"~STUDENT"<<endl;
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

void func(STUDENT obj){}

int main()
{
    STUDENT st("zhangsan",100,80);

    func(st);

    return 0;
}

结果为:

~STUDENT
~STUDENT

上边的程序中我们只定义了一个变量,但是却调用了两次析构函数,原因就在于调用函数 func 时发生了拷贝构造。修改为:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    ~STUDENT()
    {
        cout<<"~STUDENT"<<endl;
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

void func(STUDENT &obj){}

int main()
{
    STUDENT st("zhangsan",100,80);

    func(st);

    return 0;
}

就只会发生一次调用,原因在于函数中的参数形式是一个引用,而引用只是一个变量别名,扩展了变量的作用域而已,并不发生拷贝构造。

this指针

观察上一个程序的构造函数:

STUDENT(char *name, int num, int grade):num(num),grade(grade)
{
    this->name = new char[strlen(name)+1];
    strcpy(this->name,name);
}

在上边的函数中,函数形参是 name,但是数据成员中也有 name,但我们使用了 this 指针来区分两者。

作用

  • 类对象在被创建时,会自动生成指向当前对象的指针
  • 避免函数形参与数据成员同名
  • 基于 this 指针的自身引用也广泛使用于多重串联调用的函数中

多重串联调用的用法有点类似于流对象的用法:

#include <iostream>
#include <string.h>

using namespace std;

class A
{
public:
    A(int data = 0):data(data){}

    A &display()
    {
        this->data++;
        cout<<data<<endl;
        return *this;
    }

    ~A(){}

private:
    int data;
};

int main()
{
    A obj;

    obj.display().display().display();

    return 0;
}

结果为:

1
2
3

上边的程序借由 this 指针实现了函数的多重串联调用。

= 重载

定义

class classname
{
    A & operator=(const classname &obj)
    {
        statement;
        return *this;
    }
}

作用

对运算符 = 进行重载可以实现类对象之间的赋值。

规则

  • = 重载默认为等位拷贝,也就是浅赋值
  • = 重载的深赋值需要自实现
  • 返回引用,且不必使用 const 修饰
  • 如果没有对 = 重载进行显式构建,则系统默认为该类分配一个 = 重载
  • 如果对 = 重载进行显式构建,则默认 = 重载消失,以自实现为准

浅赋值和深赋值

这一点跟拷贝构造要说明的问题一样,只是拷贝是从无到有,而赋值重载则是覆盖重写。但是深赋值还需要考虑其它问题。

内存泄漏

拷贝构造发生在对象创建时,将源对象拷贝到拷贝对象当中。如果源对象存在堆上空间,只需要对应的申请同等大小的堆上空间,然后再将源对象的堆上内容复制一份即可。

而 = 重载的前提是先构建了两个对象,然后将源对象赋值到赋值对象。因此源对象和赋值对象对应的堆上空间可能是不同的:

在上图中,如果赋值对象直接申请同等大小的堆上空间,则会造成原来申请的堆上空间的泄露。因此需要先释放原来的堆上空间,再申请空间,再进行复制。

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);

    STUDENT sd("lisi",101,90);

    st.display();
    sd.display();

    sd = st;

    st.display();
    sd.display();

    return 0;
}

 结果为:

name is zhangsan
num is 100
grade is 80
name is lisi
num is 101
grade is 90
name is zhangsan
num is 100
grade is 80
name is zhangsan
num is 100
grade is 80

上边的 = 重载只是在拷贝构造的基础之上,释放了赋值对象的 name 空间,然后返回了 *this。

自赋值

但是上边的程序却无法实现 st = st 的操作,因为会将原来 name 指向的空间给释放掉,因此对于上边的 = 重载函数,可以修改为:

STUDENT & operator =(const STUDENT &obj)
{
    if(this == &obj)
        return *this;

    delete [](this->name);

    this->name = new char[strlen(obj.name)+1];
    strcpy(this->name,obj.name);
    this->num = obj.num;
    this->grade = obj.grade;

    return *this;
}

这样就能够解决自赋值的问题。

A=B=C/(A=B)=C

通常 = 重载前不加 const,所造成的差别主要在 A=B=C/(A=B)=C 能够实现。

  • 如果 = 重载前没有 const 则可以实现 A=B=C/(A=B)=C,即等式串联和表达式赋值
  • 如果 = 重载前有 const 则只能实现 A=B=C,原因在于 = 重载的返回值为 const,是不可修改的

返回栈上引用和对象

C++ 返回栈变量

#include <iostream>

using std::cout;
using std::endl;

int func()
{
    int a = 1;
    return a;
}

int main()
{
    cout<<func()<<endl;

    return 0;
}

函数 func 在发生调用时,先在栈上产生了变量 a=1,然后将该变量返回。正常情况下,函数调用结束时该函数在栈上的变量会被释放。但同时返回值会被暂时存储在寄存器上,然后函数返回,完成赋值。

C++ 返回栈对象

测试类为:

class TEST
{
public:
    TEST()
    {
        cout<<"TEST()"<<endl;
    }

    TEST(const TEST & obj)
    {
        cout<<"TEST(const TEST & obj)"<<endl;
    }

    TEST & operator=(const TEST & obj)
    {
        cout<<"TEST & operator=(const TEST & obj)"<<endl;
        return *this;
    }

    ~TEST()
    {
        cout<<"~TEST()"<<endl;
    }
};

传值

void func(TEST var){}

int main()
{
    TEST t;

    func(t);

    return 0;
}

 结果为:

TEST()
TEST(const TEST & obj)
~TEST()
~TEST()

该部分内容之前提到过,在传值时发生了拷贝构造,因此就是一次构造,一次拷贝构造,一次析构。

传引用

void func(TEST &var){}

int main()
{
    TEST t;

    func(t);

    return 0;
}

结果为:

TEST()
~TEST()

该部分内容之前提到过,引用只是变量别名,扩展了作用域,因此发生了一次构造,一次析构。

返回对象(无人接)

TEST func(TEST &var){return var;}

int main()
{
    TEST t;

    func(t);

    return 0;
}

结果为:

TEST()
TEST(const TEST & obj)
~TEST()
~TEST()

返回值先被拷贝到临时空间中,因此发生了一次构造,一次拷贝构造,二次析构。

返回对象(新对象接)

TEST func(TEST &var)
{
    cout<<static_cast<void *>(&var)<<endl;
    return var;
}

int main()
{
    TEST t;

    TEST tt = func(t);
    cout<<static_cast<void *>(&tt)<<endl;

    return 0;
}

结果为:

TEST()
0x61fe9f
TEST(const TEST & obj)
0x61fe9e
~TEST()
~TEST()

和之前的结果一样,只是临时空间变成了对象 tt 的空间,也是发生了一次构造,一次拷贝构造,二次析构。

返回对象(已存在对象接)

TEST func(TEST &var)
{
    cout<<static_cast<void *>(&var)<<endl;
    return var;
}

int main()
{
    TEST t;

    TEST tt;
    tt = func(t);
    cout<<static_cast<void *>(&tt)<<endl;

    return 0;
}

结果为:

TEST()
TEST()
0x61fe9e
TEST(const TEST & obj)
TEST & operator=(const TEST & obj)
~TEST()
0x61fe9d
~TEST()
~TEST()

此时先构造 t 和 tt,然后函数 func 返回值通过拷贝构造产生了中间变量,中间变量再向 tt 赋值。因此发生了 两次构造,一次拷贝构造,一次赋值,三次析构。

无形参返回引用

TEST &func()
{
    TEST var;
    return var;
}

int main()
{
    TEST t = func();

    return 0;
}

结果为:

TEST()
~TEST()
TEST(const TEST & obj)
~TEST()

首先是函数 func 中 var 的构造,析构。然后返回值的临时变量拷贝到 t,最后析构 t。因此发生了一次构造,一次拷贝构造,两次析构。

因为返回的是已经析构了的对象,不知道会发生什么,因此不能这么使用。

有形参返回引用

比如之前实现的 = 重载的形式,能够实现函数的多重串联调用。

栈和堆上的对象及对象数组

类对象的定义和初始化

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT sarr[3] = {STUDENT("zhangsan",100,60),
                       STUDENT("lisi",101,70),STUDENT("wangwu",101,80)};

    STUDENT *p = new STUDENT[3]{STUDENT("zhangsan",100,60),
                       STUDENT("lisi",101,70),STUDENT("wangwu",102,90)};     // C11

    for(int i=0;i<3;i++)
        sarr[i].display();

    for(int i=0;i<3;i++)
        p[i].display();

    return 0;
}

结果为:

name is zhangsan
num is 100
grade is 60
name is lisi
num is 101
grade is 70
name is wangwu
num is 101
grade is 80
name is zhangsan
num is 100
grade is 60
name is lisi
num is 101
grade is 70
name is wangwu
num is 102
grade is 90

注意事项

  • 使用 new 创建一个类对象会调用对应的构造函数,相当于定义一个类对象。
  • new 构建的类对象需要使用 delete 释放,释放时默认调用析构函数
  • 不论在栈上还是堆上生成的数组,都会调用构造函数,调用次数为数组元素个数
  • 类对象数组初始化形式比较固定,不能部分初始化,只能全部初始化。

类对象的存储方式

之前在 C 语言中曾经提到过,函数名也是一个指针,也是存储在内存当中的。那么对于类来说是否需要存储每个对象的数据成员和成员函数呢?这里我们借用之前提到的类 STUDENT 来说明这个问题:

int main()
{
    STUDENT sd("zhangsan",100,60);

    STUDENT st("lisi",101,70);

    cout<<sizeof(sd)<<endl;
    cout<<sizeof(st)<<endl;
    cout<<sizeof(STUDENT)<<endl;

    return 0;
}

结果为:

12
12
12

上面的结果表明,该类对象的大小是恒定的,对应的空间刚好是单个对象的数据成员所占内存的大小,而不包括函数代码所占用的空间。

原理

类对象只存储对应的数据成员,而函数方法却是调用公用的函数代码段。又因为不用的对象所使用的数据成员是不同的,因此就需要一种方法去实现这种机制。这时候就用到了 this 指针。

之前提到过 this 指针指向调用该函数的不同对象,因此就能够保证能够对应找到对应的数据成员,同时又大大节省了内存空间。

成员函数无论是在类内定义还是在类外定义,成员函数代码的存储方式都相同。如果多少了解一点 python 语言的话,会发现里边的类成员函数第一个参数一般是 self。而 C++ 中的 this 指针就可以认为是这个 self。至于说哪个在前,哪个在后,则可能是 python 只是显式给出了这种原理,而 C++ 内部则将之封装了起来。

const

const 数据成员

我们在 C 语言中说过,const 修饰的成员为常量,是不能够修改的,一般需要在定义的时候完成初始化。而 const 修饰的类数据成员的性质为:

  • const 修饰的类数据成员,表示该成员为常量,不能够被修改
  • const 数据成员只能通过参数初始化列表进行初始化
  • 能够被 const 和非 const 成员函数调用,而不能进行修改

此时的类变为:

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade,int school):num(num),grade(grade),school(school)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj):school(obj.school)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display()
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
        cout<<"school is "<<school<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    const int school;
};

需要注意的是,在构造函数和拷贝构造函数中都应该至少给出 const 数据成员的参数初始化列表。

const 成员函数

定义

type funcname(argument) const
{
    statement;
}

含义

  • const 修饰成员函数表示在本函数内部不会修改类内的数据成员
  • 也不会调用其它非 const 成员函数
  • const 修饰的类成员函数声明和定义如果分开书写的话,const 都不能省略
  • const 还能加载函数头的开头,此时的 const 是修饰的返回值,而不是函数本身

const 构成函数重载

我们之前说过函数重载的规则为:

  • 函数名相同
  • 参数个数不同,参数类型不同,参数顺序不同,都能够构成函数重载
  • 返回值类型相同

而 const 用来修饰函数的话,也符合函数重载的概念,因此也能够构成重载:

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display()
    {
        name = "*******";
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
};

int main()
{
    STUDENT st("zhangsan",100,80);
    const STUDENT sd("lisi",101,90);

    st.display();
    sd.display();

    return 0;
}

结果为:

name is *******
num is 100
grade is 80
name is lisi
num is 101
grade is 90

从上边的结果可知:

  • 如果 const 构成函数重载,const 对象只能调用 const,非 const 对象优先调用非 const 函数
  • const 函数只能调用 const 函数,非 const 函数可以调用非 const 函数
  • 如果会使用到 const 对象,最好使用成员函数的 const 版本,或者将之重载
  • const 修饰的类成员函数声明和定义如果分开书写的话,const 都不能省略
  • const 还能加载函数头的开头,此时的 const 是修饰的返回值,而不是函数本身

const 对象

定义

const classname var;

规则

  • const 对象只能调用 const 成员函数
  • 可以访问 const 成员或者非 const 成员,但是不能修改

static

static 数据成员

之前在 C 中使用到 static 时,我们提到:

static 修饰局部变量时:

  • 会更改局部变量的生命周期,使其与进程一致
  • static 修饰的局部变量如果未初始化,会被初始化为 0

static 修饰全局变量时:

  • 会限制该变量的外延性,使其成为只能在本文件内部使用的全局变量
  • 避免了命名污染

但是在 C++ 中,静态成员表示该成员属于整个类,而不是某个对象,所有的对象共享定义的静态成员。因此使用静态成员变量可以实现数据在多个对象之间而不是全局范围内的共享。

不过需要申明的一点是虽然可以通过类对象调用静态成员,但是静态成员实际上是属于类的。

声明

static datatype static_var;             // 类内定义

初始化

datatype classname::static_var = init_value;             // 类外初始化

在类外进行初始化时,不需要 static 关键字。

调用

之前提到过,静态成员可以通过类对象实现调用,但是静态成员本质上还是属于类的,因此可以通过两种方法实现调用:

classname::static_var
class_obj.static_var

实例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member+this->name+" ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
        cout<<"member is "<<STUDENT::member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);
    st.display();
    STUDENT sd("lisi",101,90);
    sd.display();

    cout<<sizeof(STUDENT)<<endl;

    return 0;
}

结果为:

name is zhangsan
num is 100
grade is 80
member is zhangsan
name is lisi
num is 101
grade is 90
member is zhangsan lisi
12

在上边的例子中,借用静态成员实现了成员的统计,可以看出:

  • static 成员能够实现类间对象的信息共享
  • static 成员不占用类空间,在类外存储
  • static 成员从形式上看,是属于类的全局变量,存储在 data 区的 rw 段
  • static 和 const 成员一样必须要进行初始化,不同的是,static 成员需要在类外初始化
  • 可以通过命名空间即 classname::static_var 访问,也可以通过对象访问的形式 class_obj.static_var 访问

static 成员函数

声明

static datatype funcname(argument)
{
    statements;
}

作用

用来管理静态成员,静态函数只能够访问静态成员。

调用方式

同静态成员类似,也可以通过两种方式进行调用:

classname::static_func();
class_obj.static_func();

实例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member+this->name+" ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<STUDENT::member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);
    st.display();
    STUDENT::dismem();

    STUDENT sd("lisi",101,90);
    sd.display();
    STUDENT::dismem();

    return 0;
}

结果为:

name is zhangsan
num is 100
grade is 80
member is zhangsan
name is lisi
num is 101
grade is 90
member is zhangsan lisi

上边的结果跟之前的结果一样,从上边的结果也可以看出:

  • 静态成员函数的意义只是就为了管理静态数据成员,完成对静态数据成员的访问,能够更好的完成对静态数据成员的封装
  • 静态成员函数只能访问静态数据成员。可以想想如果使用 classname::static_func() 的形式访问,就会无法确定 this 指针指向的变量
  • 静态成员函数还不能够被 const 修饰。因为 const 成员函数用来修饰该成员函数的隐式 this 指针为 const,而 static 成员函数中不存在 this 指针 

static const

const 数据成员不可更改,static 数据成员可以实现类内共享。如果一个类数据成员既不能够被改变,也要在类内共享,就需要使用 static const 修饰。

  • 修饰数据成员,必须要在类内初始化,static const 合写在函数头的最前边,static const 谁先谁后无所谓
  • 修饰成员函数,static const 合写在函数头的最前边,static const 谁先谁后无所谓
#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    const static void dismem()
    {
        cout<<"member number is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

private:
    char *name;
    int num;
    int grade;
    static const int member = 100;
};

int main()
{
    STUDENT st("zhangsan",100,80);
    st.display();
    st.dismem();

    return 0;
}

结果为:

name is zhangsan
num is 100
grade is 80
member number is 100

注意事项 

在测试上边实例的时候,发现只能使用 integral 或者枚举类型的数据对 static const 类型的数据进行初始化。对于该现象的解释为:

If a static data member is of const integral or const enumeration type, its declaration in the class definition can specify a constant-initializer which shall be an integral constant expression . In that case, the member can appear in integral constant expressions. The member shall still be defined in a namespace scope if it is used in the program and the namespace scope definition shall not contain an initializer.

什么是 intergral 类型?

bool、char、wchar_t 包含被 signed 或者 unsigned 修饰的情况,统称integral类型,integral 类型的同义词是 integer 类型。

指向类成员的指针

定义指针,使其指向类数据成员或者成员函数,然后通过指针访问类的成员。

指向类数据成员的指针

定义

datatype classname::*pointer

初始化

datatype classname::*pointer = &classname::non_static

指向非静态数据成员的指针在定义时必须和类相互关联,在使用时必须和具体的对象相互关联。

解引用

使用该种类型的指针时,需要先指定类的一个对象,然后通过对象引用指针指向的成员。

class_obj.*pointer
class_obj_pointer->*pointer

实例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member + this->name + " ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

public:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);

    char * STUDENT::*p;
    p = &STUDENT::name;

    cout<<st.*p<<endl;

    return 0;
}

结果为:

zhangsan

但是这种使用方式需要打开数据成员的权限,会有点打破封装的形式。

指向成员函数的指针

非静态成员函数由三部分构成:

  • 参数列表
  • 返回类型
  • 所属类型

因此指向该成员函数的指针应该也在这三方面与之保持相同。

定义

datatype (classname::*pointer)(argument list)

初始化

datatype (classname::*pointer)(argument list) = &classname::non_static

解引用

使用该种类型的指针时,需要先指定类的一个对象,然后通过对象引用指针指向的成员。

(class_obj.*pointer)(argument list)
(class_obj_pointer->*pointer)(argument list)

实例

#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member + this->name + " ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

public:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);

    void (STUDENT::*p)() const;
    p = &STUDENT::display;

    (st.*p)();

    return 0;
}

结果为:

name is zhangsan
num is 100
grade is 80

该成员函数对应的 const 不要省略。

指向类静态成员的指针

  • 指向静态数据成员的指针的定义和使用与普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联。
  • 指向静态成员函数的指针和普通指针相同,在定义时无须和类相关联,在使用时也无须和具体的对象相关联。
     
#include <iostream>
#include <string.h>

using namespace std;

class STUDENT
{
public:
    STUDENT(char *name, int num, int grade):num(num),grade(grade)
    {
        this->name = new char[strlen(name)+1];
        strcpy(this->name,name);
        member = member + this->name + " ";
    }

    STUDENT(const STUDENT &obj)
    {
        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;
    }

    STUDENT & operator =(const STUDENT &obj)
    {
        delete []name;

        this->name = new char[strlen(obj.name)+1];
        strcpy(this->name,obj.name);
        this->num = obj.num;
        this->grade = obj.grade;

        return *this;
    }

    void display() const
    {
        cout<<"name is "<<name<<endl;
        cout<<"num is "<<num<<endl;
        cout<<"grade is "<<grade<<endl;
    }

    static void dismem()
    {
        cout<<"member is "<<member<<endl;
    }

    ~STUDENT()
    {
        delete []name;
    }

public:
    char *name;
    int num;
    int grade;
    static string member;
};

string STUDENT::member = "";

int main()
{
    STUDENT st("zhangsan",100,80);

    string *p = &STUDENT::member;

    cout<<*p<<endl;

    static void (*pp)() = &STUDENT::dismem;

    pp();

    return 0;
}

此时要注意不管是静态数据成员还是函数成员,在创建指针进行指向的时候,都是采用的是类外定义的形式:

string *p = &STUDENT::member;

static void (*pp)() = &STUDENT::dismem;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章