C++之类和对象(二)

来源:互联网 发布:哪个军区实力最强 知乎 编辑:程序博客网 时间:2024/06/05 09:26

构造函数和析构函数

1.构造函数

C++中,有一种特殊的成员函数,它的名字和类名相同,没有返回值,不需要用户显式调用(用户也不能调用),而是在创建对象时自动执行。这种特殊的成员函数就是构造函数(Constructor)。

 

Student类中定义了一个构造函数Student(char *, int, float),它的作用是给三个private属性的成员变量赋值。要想调用该构造函数,就得在创建对象的同时传递实参,并且实参由( )包围,和普通的函数调用非常类似。

 

上创建对象时,实参位于对象名后面,例如Student stu("小明", 15, 92.5f);在上创建对象时,实参位于类名后面,例如new Student("李华", 16, 96)

 

构造函数必须是 public属性的,否则创建对象时无法调用。当然,设置为privateprotected属性也不会报错,但是没有意义。


构造函数没有返回值,因为没有变量来接收返回值,即使有也毫无用处,这意味着:

不管是声明还是定义,函数名前面都不能出现返回值类型,即使是 void也不允许;

函数体中不能有 return语句。

 

2.构造函数的重载

和普通成员函数一样,构造函数是允许重载的。一个类可以有多个重载的构造函数,创建对象时根据传递的实参来判断调用哪一个构造函数。

 

构造函数的调用是强制性的,一旦在类中定义了构造函数,那么创建对象时就一定要调用,不调用是错误的。如果有多个重载的构造函数,那么创建对象时提供的实参必须和其中的一个构造函数匹配;反过来说,创建对象时只有一个构造函数会被调用。

如果用户自己没有定义构造函数,那么编译器会自动生成一个默认的构造函数,只是这个构造函数的函数体是空的,也没有形参,也不执行任何操作。比如上面的 Student类,默认生成的构造函数如下:

Student(){}

 

一个类必须有构造函数,要么用户自己定义,要么编译器自动生成。一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成。在示例1中,Student类已经有了一个构造函数Student(char *, int, float),也就是我们自己定义的,编译器不会再额外添加构造函数Student(),在示例2中我们才手动添加了该构造函数。

实际上编译器只有在必要的时候才会生成默认构造函数,而且它的函数体一般不为空。默认构造函数的目的是帮助编译器做初始化工作,而不是帮助程序员。这是C++的内部实现机制,这里不再深究,初学者可以按照上面说的“一定有一个空函数体的默认构造函数”来理解。

 

最后需要注意的一点是,调用没有参数的构造函数也可以省略括号。对于示例2的代码,在栈上创建对象可以写作

Student stu()

Student stu

在堆上创建对象可以写作

Student *pstu = new Student()

Student *pstu = new Student

它们都会调用构造函数 Student()

 

3.构造函数的参数初始化表

构造函数的一项重要功能是对成员变量进行初始化,为了达到这个目的,可以在构造函数的函数体中对成员变量一一赋值,还可以采用参数初始化表。

 

定义构造函数时并没有在函数体中对成员变量一一赋值,其函数体为空(当然也可以有其他语句),而是在函数首部与函数体之间添加了一个冒号:,后面紧跟m_name(name), m_age(age), m_score(score)语句,这个语句的意思相当于函数体内部的m_name = name; m_age = age; m_score = score;语句,也是赋值的意思。

 

使用参数初始化表并没有效率上的优势,仅仅是书写方便,尤其是成员变量较多时,这种写法非常简明明了

 

参数初始化表可以用于全部成员变量,也可以只用于部分成员变量。下面的示例只对 m_name使用参数初始化表,其他成员变量还是一一赋值:

 

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

    m_age = age;

    m_score = score;

}

 

#include <iostream>

using namespace std;

class Demo{

private:

    int m_a;

    int m_b;

public:

    Demo(int b);

    void show();

};

Demo::Demo(int b): m_b(b), m_a(m_b){ } //构造函数对成员变量初始化

void Demo::show(){ cout<<m_a<<", "<<m_b<<endl; }

int main(){

    Demo obj(100);

    obj.show();

    return 0;

}

注意,参数初始化顺序与初始化表列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关在参数初始化表中,我们将 m_b放在了 m_a 的前面,看起来是先给 m_b赋值,再给 m_a赋值,其实不然!成员变量的赋值顺序由它们在类中的声明顺序决定,在 Demo类中,我们先声明的 m_a,再声明的m_b

 

Demo::Demo(int b): m_b(b), m_a(m_b){

    m_a = m_b;

    m_b = b;

}

4.初始化const成员变量

参数初始化表还有一个很重要的作用,那就是初始化 const 成员变量。初始化 const成员变量的唯一方法就是使用参数初始化表。

 

 

 

 

 

正确:        

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

}

错误:

class VLA{

private:

    const int m_len;

    int *m_arr;

public:

    VLA(int len);

};

VLA::VLA(int len){

    m_len = len;

    m_arr = new int[len];

}

 

 

5.析构函数

建对象时系统会自动调用构造函数进行初始化工作同样,销毁对象时系统也会自动调用一个函数来进行清理工作,例如释放分配的内存、关闭打开的文件等,这个函数就是析构函数。

 

析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。

 

注意:析构函数没有参数,不能被重载因此一个类只能有一个析构函数。如果用户没有定义,编译器会自动生成一个默认的析构函数。

 

 

我们定义了一个 VLA类来模拟变长数组,它使用一个构造函数为数组分配内存,这些内存在数组被销毁后不会自动释放,所以非常有必要再添加一个析构函数,专门用来释放已经分配的内存。

~VLA()就是 VLA 类的析构函数,它的唯一作用就是在删除对象(第 53行代码)后释放已经分配的内存。

 

函数名是标识符的一种,原则上标识符的命名中不允许出现~符号,在析构函数的名字中出现的~可以认为是一种特殊情况,目的是为了和构造函数的名字加以对比和区分。

 

注意:at()函数只在类的内部使用,所以将它声明为 private属性;m_len变量不允许修改,所以用 const限制。

 

C++ 中的 newdelete 分别用来分配和释放内存,它们与C语言中malloc()free()最大的一个不同之处在于:用 new分配内存时会调用构造函数,用 delete释放内存时会调用析构函数。构造函数和析构函数对于类来说是不可或缺的,所以在C++中我们非常鼓励使用new delete

 

6.构造函数的执行时机

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

在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。

在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。

new 创建的对象位于堆区,通过delete 删除时才会调用析构函数;如果没有delete,析构函数就不会被执行。

 

7.this 指针

this C++中的一个关键字,也是一个 const指针它指向当前对象通过它可以访问当前对象的所有成员。

 

void Student::setname(char *name){

    this->name = name;

}

void Student::setage(int age){

    this->age = age;

}

void Student::setscore(float score){

    this->score = score;

}

 

this 虽然用在类的内部,但是只有在对象被创建以后才会给 this赋值,并且这个赋值的过程是编译器自动完成的,不需要用户干预,用户也不能显式地给 this赋值。

 

this const指针,它的值是不能被修改的,一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。

this 只能在成员函数内部使用,用在其他地方没有意义,也是非法的。

只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用(后续会讲到static 成员)。

 

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

 

this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this赋值

 

8.static静态成员变量

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

有时候我们希望在多个对象之间共享数据,对象 a 改变了某份数据后对象 b 可以检测到。

C++中,我们可以使用静态成员变量来实现多个对象共享数据的目标。静态成员变量是一种特殊的成员变量,它被关键字static修饰

class Student{

public:

    Student(char *name, int age, float score);

    void show();

public:

    static int m_total;  //静态成员变量,BBS数据段

private:

    char *m_name;

    int m_age;

    float m_score;

};

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

 

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

type class::name = value;

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

int Student::m_total = 0;

 

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

 

注意static成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化时分配。反过来说,没有在类外初始化的 static成员变量不能使用。

 

static 成员变量既可以通过对象来访问,也可以通过类来访问。请看下面的例子:

//通过类类访问 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;

这三种方式是等效的。

注意:static 成员变量不占用对象的内存,而是在所有对象之外开辟内存,即使不创建对象也可以访问。具体来说,static成员变量和普通的 static变量类似,都在内存分区中的全局数据区分配内存。

 

9.几点说明

1) 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。

2) static 成员变量和普通 static变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。

3) 静态成员变量必须初始化,而且只能在类体外进行。例如:

int Student::m_total = 10;

初始化时可以赋初值,也可以不赋值。如果不赋值,那么会被默认初始化为 0。全局数据区的变量都有默认的初始值0,而动态数据区(堆区、栈区)变量的默认值是不确定的,一般认为是垃圾值。

4) 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 privateprotectedpublic关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。

 

10.static静态成员函数

在类中,static 除了可以声明静态成员变量,还可以声明静态成员函数。普通成员函数可以访问所有成员(包括成员变量和成员函数)静态成员函数只能访问静态成员。

编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给this,所以普通成员函数只能在创建对象后通过对象来调用,因为它需要当前对象的地址。而静态成员函数可以通过类来直接调用,编译器不会为它增加形参this,它不需要当前对象的地址,所以不管有没有创建对象,都可以调用静态成员函数。

 

 

 

普通成员变量占用对象的内存,静态成员函数没有 this指针,不知道指向哪个对象,无法访问对象的成员变量,也就是说静态成员函数不能访问普通成员变量,只能访问静态成员变量。

普通成员函数必须通过对象才能调用,而静态成员函数没有 this 指针,无法在函数体内部访问某个对象,所以不能调用普通成员函数,只能调用静态成员函数。

静态成员函数与普通成员函数的根本区别在于:普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。

 

和静态成员变量类似,静态成员函数在声明时要加 static,在定义时不能加static。静态成员函数可以通过类来调用(一般都是这样做),也可以通过对象来调用,上例仅仅演示了如何通过类来调用。

 

11.const关键字

在类中,如果你不希望某些数据被修改,可以使用const关键字加以限定。const可以用来修饰成员变量、成员函数以及对象。

 

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

 

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

 

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;

}

 

getname()getage()getscore()三个函数的功能都很简单,仅仅是为了获取成员变量的值,没有任何修改成员变量的企图,所以我们加了const 限制,这是一种保险的做法,同时也使得语义更加明显。

需要注意的是,必须在成员函数的声明和定义处同时加上 const关键字char *getname() constchar *getname()是两个不同的函数原型,如果只在一个地方加const 会导致声明和定义处的函数原型冲突。

 

12.const对象

const 也可以用来修饰对象,称为常对象。一旦将对象定义为常对象之后,就只能调用类的 const 成员了。

 

定义常对象的语法和定义常量的语法类似:

const  class  object(params);

class const object(params);

当然你也可以定义 const 指针:

const class *p = new class(params);

class const *p = new class(params);

class为类名,object为对象名,params为实参列表,p为指针名。两种方式定义出来的对象都是常对象。

 

一旦将对象定义为常对象之后,不管是哪种形式,该对象就只能访问被 const 修饰的成员函数。

但成员变量的访问不限制,但不同通过其进行修改

 

13.class struct的区别

C++ 中保留了C语言的struct 关键字,并且加以扩充。在C语言中,struct只能包含成员变量,不能包含成员函数。而在C++中,struct类似于 class,既可以包含成员变量,又可以包含成员函数。

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

使用 class 时,类中的成员默认都是private 属性的;而使用struct 时,结构体中的成员默认都是public 属性的。

class 继承默认是 private继承,而 struct继承默认是 public继承。

class 可以使用模板,而 struct不能。

C++ 没有抛弃C语言中的struct 关键字,其意义就在于给C语言程序开发人员有一个归属感,并且能让C++编译器兼容以前用C语言开发出来的项目。

 

一个类中可以有 publicprotectedprivate三种属性的成员,通过对象可以访问 public成员,只有本类中的函数可以访问本类的 private成员。现在,我们来介绍一种例外情况——友元(friend)。借助友元(friend),可以使得其他类中的成员函数以及全局范围内的函数访问当前类的private 成员

 

fnend 的意思是朋友,或者说是好友,与好友的关系显然要比一般人亲密一些。我们会对好朋友敞开心扉,倾诉自己的秘密,而对一般人会谨言慎行,潜意识里就自我保护。在C++中,这种友好关系可以用friend 关键字指明,中文多译为“友元”,借助友元可以访问与其有好友关系的类中的私有成员。如果你对“友元”这个名词不习惯,可以按原文friend 理解为朋友。

 

友元函数

 

在当前类以外定义的、不属于当前类的函数也可以在类中声明,但要在前面加 friend 关键字,这样就构成了友元函数。友元函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数。

 

友元函数可以访问当前类中的所有成员,包括 publicprotectedprivate属性的。

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

 

friend 函数不仅可以是全局函数(非成员函数),还可以是另外一个类的成员函数。