Class_And_Object


类和目标概述

区别面向进程和面向目标

面向进程数据和操作是别离的,每一个操作都是独立的,这儿拿学生举例:

//以下是面向进程所对应的学生操作:
struct Student
{
    char name[24];
    int age;
    char sex;
};
void init_student(struct Student* stu, char* str, int age, char sex);
void input_age(struct Student* stu, int age);
void output_age(struct Student* stu);

而面向目标中数据和操作是被封装在类中的:(封装是对数据和操作数据的方法的有机结合,躲藏目标的特点和完结细节的一种做法,仅对外公开接口来和目标进行交互,封装实质上是一种更严厉的管理)

class Student
{
private:
    char name[24];
    int age;
    char sex;
public:
	Student(char* str, int age, char sex);
	void input_age(int age);
	void output_age();
};

在面向进程的示例中,我们运用了结构体和函数来描述学生的特点和对信息进行操作。结构体用于封装学生的特点,而函数则用于对学生信息进行操作。函数是对数据的操作进程的封装,数据和操作是别离的。面向进程编程着重按照指定的进程逐渐解决问题,代码的流程直观,从上到下逐渐履行。

在面向目标的示例中,我们界说了一个类 Student,其间封装了学生的特点和操作学生信息的函数。类将数据(特点)和操作函数(方法)封装在一起,目标是类的实例,它拥有类界说的特点和方法。面向目标编程着重目标的概念和封装性,经过创立目标来完结操作。类供给了一种模板,允许我们根据需求创立多个目标,而且每个目标都具有自己的特点和行为,这更相似于现实生活中的目标交互。

类的声明与界说

class Thepale; //类的声明
class Thepale //类的界说
{
private:
//...
public:
//...
};

这儿需求留意区别类和函数的运用方法,函数能够前向声明告知编译器函数存在,经过符号表匹配后调用;但类的前向声明只能告知编译器该类的类型存在,没有相似于函数的符号表最后能够找到类的界说中的详细内容,所以类的运用一般选用直接界说。所以会发现,这样的代码能够运转:

int func();
int main()
{
	printf("%d", func());
	return 0;
}
int func() { return 521; }

而这样的代码不允许运转:

class Thepale;
int main()
{
	printf("%d", sizeof(Thepale));
	return 0;
}
class Thepale
{
};

类的拜访限定符

public(公有的)、protected(维护的)、private(私有的)。

class Thepale
{
public:
    int a;
    void func1() { std::cout << "Thepale" << std::endl; }
protected:
	int b;
    void func2() { std::cout << "Thepale" << std::endl; }
private:
    int c;
    void func3() { std::cout << "Thepale" << std::endl; }
};
int main()
{
    Thepale obj;
    //public:
	obj.a;
    obj.func1();
    //protected:
    obj.b;
    obj.func2();
    //private:
    obj.c;
    obj.func3();
    return 0;
}

拜访权限效果域从该拜访限定符出现的方位开端直到下一个拜访限定符出现时停止,若后续没有拜访限定符出现,则到大括号完毕处停止

在以上函数中,只要 public 的拜访部分是合法的,其它部分均会报错,在这儿 private 和 protected 是相似的(详细区别在继承中讲解)。关于公有部分能够被任意拜访,私有和维护部分只能在类中被拜访。故可知其体现了封装特性。

在 C++ 中,将 C 言语的 struct 升级为类,若没有拜访限定符,struct 的默许特点是 public(为了兼容 C),class 的默许特点是 private

类的成员变量和成员函数

class Thepale
{
    //成员变量
    int a;
    int b;
    int c;
    //成员函数
    int func1() { return a; }
    int func2() { return b; }
    int func3() { return c; }
};

类完结数据和方法的封装,以上所界说的变量等为成员变量,成员变量能够是基本数据类型,自界说类型,指针类型,数组类型等一切类型,也能够有 const 或 static 特点,这儿仅仅做一个总称。所界说的函数为成员函数,包括自界说成员函数,结构函数,析构函数等,这儿相同仅仅做一个总称。

类的实例化

类一般作为图纸、结构存在,例如每个人都会有年龄,身高,体重,名字,但每个人的这些特点并不必定相同,类能够实例化出各种各样不同的目标。类也能够比作房屋的图纸,告知哪个地方需求怎么建造,它并没有实践空间,只要实例化出的目标(根据图纸建出的房子)才有实践空间。

类的效果域

在以下代码中,所选用的成员函数界说方法是声明和界说同行:

class Thepale
{
    int a;
    int func() { return a; } //声明和界说
};

在更多的应用场景中,将声明和界说别离:

class Thepale
{
    int a;
    int func();
};
int Thepale::func() { return a; } //需求用域效果限定符指定函数所在的类域,且该函数能够运用类中的成员变量

相似于这样的操作是不允许的:

Thepale::a;

由于类仅仅是图纸,无法在图纸中找到房子的实体,这种操作是大错特错的。

Thepale::func();

这也是不允许的,涉及到 this 指针,由于成员函数无法显式传递 this 指针,故这样的调用会导致参数缺失,编译器一般所报出的是:调用非静态成员函数需求一个目标,道理共同,实质需求的是 [this 指针](#This 指针)

类的存储方法与巨细核算

class Thepale
{
    int a = 0;
    char b = 0;
    void func1() {}
    void func2() {}
};

选用 sizeof 核算上述类的巨细,成果是:8 byte

其和结构体占用内存的核算方法共同,遵循结构体内存对齐原则。1

成员函数是被放在公共代码段的,假如实例化一个目标就需求创立一份成员函数,将会导致大量的代码冗余,故类中实在存储的只要成员变量,所以核算巨细也是核算成员变量的巨细。

class Thepale
{};

关于空类而言,其 sizeof 所核算的巨细仍有 1 byte,由于有必要让类所实例化出的目标要占有详细的内存空间,不然 Thepale e; &e; 这儿 e 的地址就不复存在了。但这并不是 C++ 的要求,而是编译器的选择。

class Thepale
{
    void func1() {}    
};

故关于这种状况,巨细仍是 1 byte,原因已叙说,这儿仅仅做再次证明。

This 指针

class Thepale
{
public:
    int a;
    int func() { return a; }
};
int main()
{
    Thepale o1; o1.a = 521;
    Thepale o2; o2.a = 1314;
    std::cout << o1.func() << " ";
    std::cout << o2.func();
    return 0;
}

以上程序输出成果为:521 1314

但它们调用的是同一个函数,可是它们回来的 a 值并不相同。在类的效果域中就已提到 this 指针。关于 Thepale 类中的 func 函数,还能够这样完结:

int func() { return this->a; }

实质便是这个函数经过哪个目标调用,就会传递该目标的地址,在底层完结中,调用和完结应是这样的:

//调用
func(&o1);
func(&o2);
//完结
int func(Thepale* const this) { return this->a; }

this 指针是不允许显式传递和接纳的,但能够显式运用,能够经过 this 指针拜访目标中的成员变量。这也解说了 Thepale::func(); 是不行行的。从 this 指针的类型也能够知道,this 指针是不能被修正的。

若和 C 言语对比,会发现其实实质是相同的:

struct Thepale
{
    int a;
};
int func(struct Thepale* const p) { return p->a; }

仅仅 C++ 省掉了用户传递的进程,都由编译器完结,提升了可读性,使代码愈加简练,也是封装必不行少的一环。

This 指针为空

this 指针是能够为空的:

class Thepale
{
public:
    int a;
    void func() { std::cout << "hello" << std::endl; }
};
int main()
{
    Thepale* p = nullptr;
    p->func();
	return 0;
}

以上程序正常输出:hello

相当于这样调用:func(nullptr); 尽管 this 指针为空,但 func 函数中并没有任何地方运用到 this 指针(或者说没有拜访成员变量),故并不会报错。且这儿并不能看作对空指针的解引证,由于其汇编底层是这样的:

    Thepale* p = nullptr;
00007FF6D78F1EAB  mov         qword ptr [p],0  
    p->func();
00007FF6D78F1EB3  mov         rcx,qword ptr [p]  ;rcx 中装的是 this 指针的地址
00007FF6D78F1EB7  call        Thepale::func (07FF6D78F1465h) ;以空的 this 指针调用 func 函数

不行凭借字面意思了解代码。

This 指针严厉来说是存在于栈区中,由于其作为函数形参存在,但有些编译器存放在寄存器中,例如 visual studio。


六大默许成员函数

结构函数

在一个类实例化出目标时,一般都需求进行初始化,在 C 言语阶段,一般经过 xxx_initialize 来完结,但在 C++ 中,结构函数承当了这一工作,它会在目标被创立时主动调用完结初始化,以确保每个目标都被初始化,避免未初始化所带来的错误。(留意,结构函数的意思是完结初始化而非分配空间)

结构函数具有以下特性:

  • 函数名与类名相同。
  • 没有回来值。(并非指回来值为 void,而是没有回来值的选项)
  • 目标实例化时自己调用。
  • 能够被重载,满意不同的初始化状况。
  • 若未显式界说,体系会主动创立。对主动创立的结构函数对内置类型不处理,对自界说类型调用自界说类型的默许结构函数。

以下结构函数以日期类举例:

class Date
{
private:
    size_t _year;
    size_t _month;
    size_t _day;
public:
    void initialize(size_t year, size_t month, size_t day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    void print()
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
};

假如日期类是这样,那实例化目标后要调用初始化函数进行初始化:

Date d1;
d1.initialize(2022, 05, 21);

这和 C 言语并无二至,且也没有实践解决忘掉调用、初始化不标准等问题,故结构函数便由此诞生:

class Date
{
private:
    size_t _year;
    size_t _month;
    size_t _day;
public:
    Date(int year = 1, int month = 1, int day = 1) //这儿添加了缺省值
        :_year(year) //这儿运用了初始化列表,相似于赋值
        ,_month(month)
        ,_day(day)
    {}
    void print()
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
};

能够看到结构函数的称号和类名共同,且没有回来值。若需求实例化目标,这样即可:

Date d1(2022, 05, 21);

但假如出现:

Date d1;
d1.Date(2022, 05, 21); //error:不允许运用类型名

可知编译器是不允许结构函数被显式调用的,它会在目标结构时主动调用。

假如想运用缺省值初始化目标,应该这样写:

Date d1;

而不是:

Date d1();

假如写成:Date d1(); 最大的原因是会被编译器识别为函数声明,无法完结目标的结构。故假如完结无参调用,直接结构即可,不需求加括号,这算是由于抵触而不得已的做法吧。

结构函数是能够被重载的,满意不同的初始化状况:

class Date
{
private:
    size_t _year;
    size_t _month;
    size_t _day;
public:
    Date(int year, int month, int day) //带参结构
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}
    Date() //无参结构
    {
        _year = 1946;
        _month = 2;
        _day = 15;
    }
    void print()
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
};

以上两个结构函数构成函数重载,函数重载界说详细见其文章。

假如日期类不写结构函数:

class Date
{
private:
    size_t _year;
    size_t _month;
    size_t _day;
public:
	Date()
    {
        _year = 1946;
        _month = 2;
        _day = 15;
    }
    void print()
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
};
int main()
{
    Date d1;
    d1.print();
    return 0;
}

程序的输出成果为:14757395258967641292_14757395258967641292_14757395258967641292(随机值)

但实践上在没有显式写出结构函数时,编译器会主动生成无参默许结构函数,这个结构函数关于内置类型不处理,关于自界说类型调用则调用该类型的默许结构函数,故默许生成的结构函数并非一无是处。故在一些特别状况,例如用双行列完结栈时,能够只完结行列的无参默许结构函数,栈能够不完结:

class Queue
{
private:
    int _a; //假设,仅做演示
public:
    Queue()
        :_a(0)
    {}
};
class Stack
{
private:
    Queue q1;
    Queue q2;
};

Stack 不写结构函数也是能够正常结构的。

这儿需求捋清概念,默许结构函数指的是不需求传参就能调用的结构函数(全部运用缺省参数的结构函数不是严厉含义上的默许结构函数,但也可当作默许结构函数),所以假如自己写一个不需求参数就能够调用的结构函数称为默许结构函数,编译器主动生成的也是默许结构函数。(请区别不需求参数和无参)显然,默许结构函数只能存在一个,不然会存在函数调用不明确的问题,且假如类中有自己写的结构函数,则编译器不会再默许生成,不然将引发调用不明确的问题:

class Date
{
private:
    size_t _year;
    size_t _month;
    size_t _day;
public:
    Date(int year, int month, int day)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}
    void print()
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
};

假如类中的结构函数界说如上,但调用方法如下:

Date d1;

编译器会报错,由于有了结构函数之后编译器不会默许生成,而这又是一个无参结构,并没有满意条件的结构函数,故报错。

C++11 中添加了类内初始化的特性:

class Date
{
private:
    size_t _year = 1;
    size_t _month = 1;
    size_t _day = 1;
public:
    void print()
    {
        cout << _year << "_" << _month << "_" << _day << endl;
    }
};

在目标被实例化时,都会先运用成员变量的默许值先初始化,假如这时刚好没有结构函数,生成的结构函数尽管不初始化内置类型,但其仍被类内初始化初始化了;假如有结构函数但没有对成员变量初始化,则仍会用默许值代替;假如结构函数初始化了对应的成员变量,则会覆盖掉成员变量的默许值。无论如何默许值都会在结构时先初始化成员变量。


析构函数

在 C 言语中,若运用动态拓荒的空间,最后必定要 free,在 C++ 的 new 和 delete 也是相同,这个操作需求手动进行,故容易引发内存泄漏。析构函数便由此而来,它负责在目标要被毁掉时主动调用,负责开释资源。

析构函数具有以下特性:

  • 函数名和是类名前加上 ~ 号。
  • 没有回来值。
  • 目标被毁掉时主动调用。
  • 析构函数不行重载,仅存在一个。
  • 若未显式界说,体系会主动创立。主动创立的析构函数对内置类型不处理,对自界说类型调用自界说类型的析构函数。
class Thepale
{
private:
    int* _arr;
public:
    Thepale()
    {
        _arr = new int[1024]; //动态拓荒的空间
    }
    ~Thepale() //析构命名方法
    {
        delete[] _arr; //析构时开释
    }
};
int main()
{
    Thepale e;
    return 0;
}

默许生成的析构函数,和结构函数相同,对内置类型不处理,例如假如是日期类能够不同显式写明析构函数,目标生命周期完毕内置类型会主动收回;对自界说类型则去调用自界说类型的析构函数,应用场景仍然是与双栈完结行列等相似的,这儿不过多说明。


复制结构函数

复制结构函数绝大多数的状况下在复制初始化2中被调用,常见场景有三种:1.运用已存在的目标创立新目标(这种状况必定是调用复制结构进行初始化而非 operator=,只要是用已有目标创立新目标便是复制结构的场景,切勿经过符号表象解说);2.函数参数类型为类类型目标;3.函数回来值类型为类类型目标。复制结构是一种特别的结构函数,也是结构函数的重载形式,和结构函数相同具有大部分相同的特性:

  • 复制结构是结构函数的重载。
  • 函数名和类名相同。
  • 没有回来值。
  • 形参只要一个且类型有必要是目标的引证(假如是指针则成为了结构函数的重载形式,而非复制结构函数)。
  • 未显式调用会主动生成默许复制结构函数,对内置类型完结逐字节复制(浅复制 / 值复制),对自界说类型调用自界说类型的复制结构函数。
class Thepale
{
private:
    int _a;
    int _b;
public:
    Thepale(int a = 0, int b = 0)
        :_a(a)
        ,_b(b)
    {}
    Thepale(const Thepale& e) //复制结构函数
    {
        _a = e._a;
        _b = e._b;
    }
    ~Thepale()
    {}
};
int main()
{
    Thepale e1;
    Thepale e2(e1); //用 e1 复制结构 e2
    return 0;
}

复制结构函数即对一个目标中成员变量的复制,关于上述状况,仅需求完结浅复制时,乃至能够不显式写明复制结构函数,运用默许生成的即可。

class Thepale
{
private:
    int* _arr;
    int _size;
public:
    Thepale(int size = 128) //仅作演示,不考虑细节(代码功能便是拓荒特定巨细的空间)
    {
        _arr = new int[size] {0};
        _size = size;
    }
    Thepale(const Thepale& e) //复制结构
    {
        _arr = new int[e._size] {0}; //有必要重新拓荒空间
        memcpy(_arr, e._arr, 4 * e._size);
        _size = e._size;
    }
    ~Thepale()
    {
        delete[] _arr;
        _size = 0;
        _arr = nullptr;
    }
};
int main()
{
    Thepale e1;
    Thepale e2(e1); //用 e1 深复制结构 e2
    return 0;
}

以上场景只能运用深复制,若运用浅复制,则两个目标指向同一块空间(指针是内置类型,会被复制),析构两次(实践在 memcpy 时就出现空指针错误),单个目标的数据修正同时影响两个或多个目标,形成严重问题。

复制结构函数禁止传值调用,以上述代码举例,假如复制结构函数写成:Thepale(const Thepale e); 这时 e2 进行复制结构,即相当于需求将 e1 传给 e,而这个实参复制给形参的进程又会调用复制结构,导致无限调用。

Cpp-Class_And_Object

赋值运算符重载

这儿重载的含义和函数重载不同,它更多的含义是倾向于对自界说目标进行运算的运算符含义的重新界说,故称之为运算符重载。赋值运算符重载,它一般发生在两个已存在的目标之间进行赋值。

赋值运算符的特性:

  • 用户没有显现写明时,会生成默许的赋值运算符重载,内置类型完结逐字节复制,自界说类型调用自界说类型的赋值运算符重载。
class Date
{
private:
    size_t _year;
    size_t _month;
    size_t _day;
public:
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year)
        ,_month(month)
        ,_day(day)
    {}
    Date& operator=(const Date& d)
    {
        if(this != &d) //假如目标为自身则不需求进行赋值
        {
    		_year = d._year;
    		_month = d._month;
        	_day = d._day;
		}
        return *this;
    }
};
int main()
{
    Date d1(2023, 5, 21);
    Date d2(2023, 7, 16);
    d1 = d2; //调用赋值运算符重载
}

当然,其也存在深复制的状况,不再举例。

一般而言,赋值需求完结接连赋值的特性需求回来引证,这也便是代码中回来值为 Date& 的原因。而一般传参也运用引证传参以进步效率,会进行检查是否为自己给自己赋值,避免不必要的资源耗费。留意,前面所说的并不是有必要履行,仅仅给了一个常用的合理的解。

取地址操作符重载和 const 取地址操作符重载

class Date
{ 
private:
 	int _year;
 	int _month;
	 int _day;
public :
     Date* operator&() //取地址操作符重载,一般不显现写明,编译器默许生成的即回来 this 指针,除非想让取地址能取得特定的内容
     {
     	return this;
     }
     const Date* operator&() const //对 const 匹配,const 目标取地址回来 const 的指针,避免内容被改动
     {
     	return this;
     }
};

(在成员函数后加上 const,即相似于 const Date* const this,为前一个 const,避免 Date 内的数据被修正;后一个 const 是 this 指针自带的特点,不能改动 this 指针的指向。const 不能用于润饰结构或析构函数,const 也起了对调用的匹配效果,即 const 的 this 指针的匹配:const 目标取地址是 const 的 this 指针,会调用 const 的函数,由于 const 润饰的函数躲藏的形参 this 指针的参数类型是 const *)

这两者不过多叙说,一般由编译器默许生成,且很少有需求显现写明的必要。


补充说明

  • 关于以下代码(剖析复制结构在没有优化下的调用形式以及展现复制省掉的一个场景):
class Thepale
{
private:
	int _a;
public:
    Thepale(int a = 0)
    {
        cout << "Thepale(int a = 0) -- 结构函数被调用" << endl;
		_a = a;
    }
    Thepale(const Thepale& e)
    {
        cout << "Thepale(const Thepale& e) -- 复制结构函数被调用" << endl;
		_a = e._a;
    }
    ~Thepale()
    {
		cout << "~Thepale() -- 析构函数被调用" << endl;
    }
};
Thepale func()
{
    Thepale tmp;
    return tmp;
}
int main()
{
    Thepale ret = func(); //请留意此行
    return 0;
}

在运转后,程序的履行成果是:

Thepale(int a = 0) -- 结构函数被调用
~Thepale() -- 析构函数被调用

这实践上是进行了回来值优化和复制省掉之后的成果,假如不考虑这两步,注释行逻辑应该为:func 函数被调用,在 func 函数中,创立 tmp 目标,此刻 调用结构函数;履行到 return 语句,需求回来 tmp,于是将 tmp 复制给暂时变量,复制时调用了 复制结构函数 完结复制;函数调用完结,函数栈帧毁掉,此刻 tmp 目标也 调用析构函数 毁掉;然后回到 main 函数,将暂时变量赋值给 ret 目标,由于此刻是对 ret 目标的初始化(复制初始化),所以仍然 调用复制结构函数;注释行履行完后,暂时变量毁掉,调用析构函数;(暂时变量的生命周期是当前行)main 函数履行到 return,函数完毕,ret 被收回,调用析构函数。(以上状况在履行时能够看出显着运用了复制省掉削减复制,其实此种状况便是复制初始化,但并没有调用复制结构函数)

  • 一切的默许成员函数只能在类中声明,完结方位没有详细要求。
  • 以下五个操作符不行重载:::(域效果限定符)、:?(三目操作符)、.(成员拜访操作符)、sizeof(核算目标或数据类型巨细的操作符)、.*(成员指针运算符)

Footnotes

  1. 1.第一个成员从 0 偏移量开端存储。2.其它成员变量对齐到对齐数3的整数倍处。3.结构体的总巨细为最大对齐数4的整数倍。(结构体内存对齐必定程度上也取决于编译器,嵌套结构体的状况也按此规矩处理) ↩

  2. 复制初始化 = 复制结构函数 + 复制省掉(回来值优化和右值引证)。复制结构函数绝大部分状况下会调用复制结构函数,但在一些状况下会触发复制省掉则不调用复制结构函数,但这两者都叫做复制初始化。 ↩

  3. 编译器的默许对齐数和该成员所占巨细两者取较小值。 ↩

  4. 在存储元素时,每一个元素的存储都会产生一个对齐数,取这些对齐数中的最大值。 ↩