3 类和对象(封装)

3 类和对象

3.1 类的定义和对象的创建

编程习惯

  • 在实际开发中,我们通常将类的声明放在头文件中,而将成员函数的定义放在源文件中。

类定义:类声明+函数定义

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

注意:

  • 类名的开头一班大写。

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

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

3.1.1 对象创建和使用:

核心

  • 栈内对象创建
  • 堆内对象创建

1.在栈上创建对象:栈内存由程序自动管理

Student stu;
stu.成员变量;
stu.成员函数()
  • 使用一般的创建对象方法都是在栈上创建对象。

  • 在栈上创建出来的对象都有一个名字,比如 stu,使用指针指向它不是必须的。

2.在堆上创建对象:堆上的内存由程序员手动管理,必须用delete删除

Student *pstu=new Student;
pstu—>成员变量;
pstu->成员函数()
  • 使用 new 在堆上创建出来的对象是匿名的,没法直接使用,必须要用一个指针指向它,再借助指针来访问它的成员变量或成员函数。

总结:

  • 对象名字访问成员使用点号.,通过对象指针访问成员使用箭头->

3.1.2 常对象

常对象:

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

常对象:

const  class  object(params);
class const object(params);

常对象指针(指针指向的是一个常对象)

const class *p = new class(params);
class const *p = new class(params);

3.2 成员变量和成员函数讲解

3.2.0成员变量和成员函数性质

1.类成员变量和普通变量的区别:

类成员变量在定义后不占内存,普通变量定义后占内存空间

成员变量一般以m_开头

2.类成员函数和普通函数的区别:

类成员函数依赖于类,作用域为类内

普通函数是独立的,作用域为全局或者某一个命名空间

3.域解析符::

用来连接类名和函数名,指明当前函数属于哪个类

3.2.1 类内定义成员函数和类外定义成员函数的区别(内联函数)

标准的编程规范:

  • 普通函数:类体内部对函数进行声明,类外对函数进行定义
  • 内联函数:直接定义在类内部
class Student{
public:
    char *name;
    int age;
    float score;

    void say();  //内联函数声明,可以增加 inline 关键字,但编译器会忽略
};

//函数定义
inline void Student::say(){
    cout<<name<<"的年龄是"<<age<<",成绩是"<<score<<endl;
}

注意:

  1. 类体内的定义成员函数默认为内联函数
  2. 函数的声明的定义同时加上inline,但是声明的inline会被编译器忽略。
  3. 类外定义内联函数,声明定义需要在一个文件中,否则会链接失败

3.2.2 static静态成员变量详解

背景:

  • C++中不同的对象的成员变量相互独立,但是也需要在不同对象之间共享一份数据。此时用到静态成员变量

定义:

静态成员变量是一种特殊的成员变量,它被关键字static修饰。用于多个成员之间共享数据。

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;
};
int Student::m_total = 0;//类外初始化定义

性质:

  • 内存:位于全局区编译时并类外初始化后分配内存。static 成员变量的内存既不是在声明类时分配,也不是在创建对象时分配,而是在(类外)初始化《应该在编译阶段》时分配。反过来说,没有在类外初始化的 static 成员变量不能使用。
  • 只能必须在类外初始化定义,静声明时要加 static,在初始化时不能再加 static,但必须要有数据类型。被 private、protected、public 修饰的静态成员变量都可以用这种方式初始化。
  • 静态变量和普通成员变量的区别static变量和对象的创建无关
  • 初始化可以赋初值,也可以不赋初值,默认初值为0;全局数据区的默认初值为0,动态数据区(堆区和栈区)的变量的默认值不确定。
  • 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。

3.23 static静态成员函数讲解

背景:设计一个只能访问静态成员(变量和函数)的函数。

内存:位于代码区

静态成员函数和普通成员函数根本区别:

  • 普通成员函数:普通成员函数有 this 指针,可以访问所有成员(成员变量和成员函数)

  • 静态成员函数:静态成员函数没有 this 指针,只能访问静态成员(静态成员变量和静态成员函数)

本质:

  • 编译器在编译一个普通成员函数时,会隐式地增加一个形参 this,并把当前对象的地址赋值给 this,

  • 而静态成员函数可以通过类来直接调用,编译器不会为它增加形参 this,它不需要当前对象的地址。

注意事项:

  1. 声明时要加 static,在定义时不能加 static
  2. 可以由类调用,也可由对象调用

3.2.4 const成员变量和常成员函数和const(常对象)

**背景:**不想某些数据被修改,使用const修饰成员变量和成员函数

const成员变量初始化:

  • 初始化 const 成员变量只有一种方法,就是通过构造函数的初始化列表。

const常成员函数:

  • const 成员函数可以使用类中的所有成员变量,但是不能修改它们的值,(用于保护数据)一般get函数就会作为常成员函数
  • 声明和定义时,都要在函数原型结尾加上const关键字char *getname() constchar *getname()是两个不同的函数原型,如果只在一个地方加 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;
};
//定义常成员函数
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 char * getname()
  • 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const

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

const  class  object(params);
class const object(params);

3.3 类的封装

封装性质:

  • 封装是C++面向对象三大特性之一

封装的意义:

  • 将属性和行为作为一个整体,
  • 将属性和行为加以权限控制,
  • 所谓封装,是指尽量隐藏类的内部实现,只向用户提供有用的成员函数。

成员访问方式:

  • public:直接通过对象访问
  • private:类外使用对外的接口,set和get函数进行访问

三权限:

类别-类内类外--继承
public公共权限类内可以访问类外可以访问子类可以访问
protected保护权限类内可以访问类外不可以访问子类可以访问
private私有权限类内可以访问类外不可以访问子类不可以访问

注意事项:

  • 类内不指明权限的话,默认权限为private。

  • 在类的内部(定义类的代码内部),无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。

  • 在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

#include <iostream>
using namespace std;

//类的声明
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();
};

//成员函数的定义
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;
}

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

    return 0;
}

3.4 C++ 对象内存模型

3.4.1C++对象模型

类内存分析

编译器会将成员变量和成员函数分开存储:分别为每个对象的成员变量分配内存,但是所有对象都共享同一段函数代码

对象位置:(主要存储非静态成员变量)堆区或者栈区

  • 空对象占用一个内存(主要为了区分不同的空对象)

  • 普通对象根据内部非静态变量计算内存。

函数位置:代码区

C++在程序执行时,将内存大方向分为4个区域

  • 代码区:存放函数体的二进制代码,由操作系统进行管理
  • 全局区:存放全局变量静态变量和常量(字符串常量和全局常量)
  • 栈区:有编译器自动分配释放,存放函数的参数值和局部变量(还包括局部常量)
  • 堆区:由程序员分配和释放,若程序员不释放,则程序运行结束由操作系统回收。

内存四区

3.4.2 内存四区

内存四区有着不同的生命周期

程序运行前:在程序编译后,生成.exe的可执行程序,未执行该程序前分为两个区域

  • 代码区:
    • 存放CPU执行的机器指令
    • 代码区共享:频繁执行的程序只需要保存一份代码即可
    • 代码区只读:指令无法修改
  • 全局区:(包括静态区和常量区)
    • 静态区:存放全局变量和静态变量
    • 常量区:字符串常量和其他常量

​ 该区域数据在程序结束后由操作系统释放。

程序运行时

  • 栈区

    • 由操作系统管理,由编译器分配释放,主要包括:函数的参数值,返回值和局部变量
    • 函数运行结束后,系统统一收回分配的栈内存。函数返回时避免返回局部变量的地址和局部变量的引用
  • 堆区

    • 堆是由malloc/new分配的内存块,使用free/delete来释放内存,堆的申请释放工作由程序员控制,容易产生内存泄漏 .

3.5 C++函数编译原理和成员函数实现

函数的编译:

  • **C语言编译:**C语言中的函数在编译时名字不变,或者只是简单的加一个下划线_,例如,func() 编译后为 func() 或 _func()。

  • C++函数编译:C++中的函数在编译时会根据命名空间、类、参数签名等信息进行重新命名,形成新的函数名。这个重命名的过程是通过一个特殊的算法来实现的,称为名字编码

函数调用:

成员函数最终被编译成与对象无关的全局函数

  • 如果函数体中没有成员变量,那问题就很简单,不用对函数做任何处理,直接调用即可。
  • 如果成员函数中使用了成员变量,而成员变量不是全局的,不经过处理就无法在函数内部访问。因此,C++规定,编译成员函数时要额外添加一个参数**(this指针,默认参数)**,把当前对象的指针传递进去,通过指针对象的指针来访问成员变量。
    void Demo::display(){
        cout<<a<<endl;
        cout<<b<<endl;
    }
    void new_function_name(const Demo *this){
        //通过指针p来访问a、b
        cout<<this->a<<endl;
        cout<<this->b<<endl;
    }

3.6 构造函数

3.6.0 构造函数的基本定义

背景:

对象的初始化和清理也是两个非常重要的安全问题

  • 一个对象或者变量没有初始状态,对其使用后果是未知

  • 同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题

解决方法:

c++利用了构造函数析构函数解决上述问题,这两个函数将会被编译器自动调用,完成对象初始化和清理工作。

  • 对象的初始化和清理工作是编译器强制要我们做的事情,因此如果我们不提供构造和析构,编译器会提供

  • 编译器提供的构造函数和析构函数是空实现。

构造和析构定义:

  • 构造函数:主要作用在于创建对象时为对象的成员属性赋值帮助类进行初始化工作),构造函数由编译器自动调用,无须手动调用。·
  • 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作。

构造函数

构造函数的性质:

  • 名字和类名相同,不需要显示调用(创建对象时由系统自动调用)
  • 没有返回值
    • 不管是声明还是定义,函数名前面不能出现返回类型(void也不可以)
    • 函数体中不能有return语句

3.6.1 构造函数的重载

重载构造函数和重载普通函数类似。

  1. 自定义构造函数后,编译器将不会自动提供空构造的构造函数

3.6.2 默认构造函数

注意:

  • 一定有一个空函数体的默认构造函数,一个类中必须含有构造函数,如果用户不提供,编译器将自动提供一个默认的空构造函数(没有形参,函数体为空,不进行任何操作)

  • 一旦用户自己定义了构造函数,不管有几个,也不管形参如何,编译器都不再自动生成

  • 调用没有参数的构造函数也可以省略括号。

    • 在栈上创建对象可以写作Student stu()Student stu
    • 在堆上创建对象可以写作Student *pstu = new Student()Student *pstu = new Student,它们都会调用构造函数 Student()。

3.6.3 初始化列表

**背景:**成员变量的初始化有两种方式

  • 直接在函数体中对成员变量进行赋值
  • 使用初始化列表对成员变量进行赋值

格式:

  • Demo::Demo(int b): m_b(b), m_a(m_b){ }//使用初始化列表使用:将函数和初始化列表合并
#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;
}

运行结果:
2130567168, 100 
//栈上分配内存,变量的值不确定,所以先赋值为一个随机值

**构造函数运行步骤:**构造函数运行分为两个部分:使用初始化列表能使得代码简洁,提高代码运行效率

  1. 运行初始化列表
  2. 运行函数体内部。

构造函数注意事项:

  • 成员变量的初始化顺序与初始化列表中列出的变量的顺序无关,它只与成员变量在类中声明的顺序有关。
    • 声明时顺序为m_a,m_b。则初始化的顺序就是先m_a赋值,然后m_b赋值,无论他们在初始化列表里的先后顺序是啥。
  • 初始化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];
}

3.6.5 this指针

定义:

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

void show(this,int a)//this 是默认参数,不需要指定
{
}

this指针本质:

  • this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。

  • this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。

  • this,它是成员函数和成员变量关联的桥梁。通过this指针,来访问当前对象的成员变量。

注意事项:

  • this 是 const 指针,this值是不能被修改(指向无法改变),一切企图修改该指针的操作,如赋值、递增、递减等都是不允许的。
  • this 只能在成员函数内部使用,可以调用所有成员变量。用在其他地方没有意义,也是非法的。
  • 只有当对象被创建后 this 才有意义,因此不能在 static 成员函数中使用(后续会讲到 static 成员)。

3.7 析构函数

**背景:**销毁对象时,系统需要调用一个函数来进行清理工作,释放内存,关闭打开文件

析构函数性质:

  • 析构函数(Destructor)也是一种特殊的成员函数,没有返回值,不需要程序员显式调用(程序员也没法显式调用),而是在销毁对象时自动执行。
  • 构造函数的名字和类名相同,而析构函数的名字是在类名前面加一个~符号。
  • 没有参数,无法重载。一个类只能有一个析构函数,如果用户没有定义,编译器会自动生成一个默认的析构函数。
//使用析构函数释放分配的内存
#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;  //释放内存
}

析构函数调用的时机:

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

  1. 全局对象,程序结束。在所有函数之外创建的对象是全局对象,它和全局变量类似,位于内存分区中的全局数据区,程序在结束执行时会调用这些对象的析构函数。
  2. **局部对象,函数结束。**在函数内部创建的对象是局部对象,它和局部变量类似,位于栈区,函数执行结束时会调用这些对象的析构函数。
  3. **堆对象,手动delete。**new 创建的对象位于堆区,通过 delete 删除时才会调用析构函数;如果没有 delete,析构函数就不会被执行。

3.8 友元函数

背景:

C++ 中,一个类中可以有 public、protected、private 三种属性的成员,有时需要在类外访问private成员,此时可用friend来对成员变量进行访问。

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

3.8.1 非成员函数声明友元(全局函数)

  • 友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。(普通函数没有this指针,必须借助对象访问)

  • 声明时使用friend关键字,定义时不需要使用friend

#include <iostream>
using namespace std;
class Student{
public:
    Student(char *name, int age, float score);
public:
    friend void show(Student *pstu);  //将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){ }
//非成员函数
void show(Student *pstu){
    cout<<pstu->m_name<<"的年龄是 "<<pstu->m_age<<",成绩是 "<<pstu->m_score<<endl;
}

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

  • 声明时使用friend关键字,定义时不需要使用friend
#include <iostream>
using namespace std;
class Address;  //提前声明Address类
//声明Student类
class Student{
public:
    Student(char *name, int age, float score);
public:
    void show(Address *addr);
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);
    //将Student类中的成员函数show()声明为友元函数
    friend void Student::show(Address *addr);
};

提前声明

  • 一般情况下,类必须在正式声明之后才能使用;但是某些情况下(如上例所示),只要做好提前声明,也可以先使用。

  • 类的提前声明的使用范围是有限的,不可以直接使用提前声明定义对象。

    对象的创建需要分配内存,正式声明前无法知道对象需要多大内存。但在对一个类作了提前声明后,可以用该类的名字去定义指向该类型对象的指针变量(本例就定义了 Address 类的指针变量)或引用变量(后续会介绍引用),因为指针变量和引用变量本身的大小是固定的,与它所指向的数据的大小无关。

3.8.3 友元类

将类 B 声明为类 A 的友元类,那么类 B 中的所有成员函数都是类 A 的友元函数,可以访问类 A 的所有成员,包括 public、protected、private 属性的。

#include <iostream>
using namespace std;
class Address;  //提前声明Address类
//声明Student类
class Student{
public:
    Student(char *name, int age, float score);
public:
    void show(Address *addr);
private:
    char *m_name;
    int m_age;
    float m_score;
};
//声明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;  //区(市区)
};

注意事项:

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

总结:

一班没必要将整个类申明为友元类,直接申明友元函数比较安全。

3.9 C++类的作用域

一个类就是一个独立的作用域,这也是为什么在类外定义函数或静态变量需要加上类名和作用域符号(类似void Complex::get() {})。

3.9.1作用域中重定义类型

在一个作用域内是可以自己去重定义一些类型的,比如下面这样:

for (int i = 0; i < 3; i++) {
    typedef double D;
    for (int j = 0; j < 3; j++) {
        typedef char C;
    }
}

类中重定义类型
那么,类作为一个独立的作用域也就可以在类中定义一些自己的类型。

struct Node {
    typedef int Height;
    typedef int Width;
};

typename Node::Height h = 3;

3.9.2 类的编译顺序

  • 从上到下依次编译变量,类型别名,函数声明及其返回值和形参;(顺序编译
  • 当上一步完成时,再编译成员函数的函数体。

因此:

  1. 这也就解释了为什么成员函数可以访问成员变量,即使这个变量是放在类的最末尾(后于这个成员函数定义)。原因就是编译器先编译整个类(除成员函数的函数体),然后再编译成员函数体。
  2. 这还解释了为什么在类中重命名一些类型尽量放到类的最开始处。因为类中的除成员函数体之外都是顺序编译的,如果不把类型重命名放到使用该重命名的类型之前,那么编译器就不会认识这个重命名的类型。

3.9.3 成员函数变量名的查找顺序

typedef int Height;

Height h = 5;

struct Node {
    void func(Height h) {
        h = 3;          // 1. 成员函数func 中的形参 h = 3
        this->h = 3;    // 2. 成员变量 h = 3
        Node::h = 3;    // 3. 成员变量 h = 3
        ::h = 3;        // 4. 全局范围中 h = 3
    }
private:
    Height h;
};

变量名的查找都是先到包含它最小的作用域查找,如果找不到,再到上一层的作用域查找,最后直到找到全局作用域。

查找顺序:

  • 成员函数中的局部变量
  • 类的成员变量(this指针,或者类作用域符号)
  • 全局变量(全局作用域符号)

3.10对象数组

数组性质:

  • 数组中所有的元素必须是有相同的数据类型,包括简单的数据类型(int,float)和复杂数据类型(结构体和对象)
//Thisheader file contains the Circle class declaration.
circle.h
#ifndef CIRCLE_H
#define CIRCLE_H
#include <cmath>
class Circle
{
    private:
        double radius; // Circle radius
        int centerX, centerY; // Center coordinates
    public:
        Circle() // Default constructor
        {
            // accepts no arguments
            radius = 1.0;
            centerX = centerY = 0;
        }
        Circle(double r) // Constructor 2
        {
            // accepts 1 argument
            radius = r;
            centerX = centerY = 0;
        }
        Circle(double r, int x, int y) // Constructor 3
        {
            // accepts 3 arguments
            radius = r;   
            centerX = x;
            centerY = y;
        }
      
}; // End Circle class declaration
#endif

circle.cpp

// This program uses an array of objects.
// The objects are instances of the Circle class.
#include <iostream>
#include <iomanip>
#include "Circle.h" // Circle class declaration file
using namespace std;
const int NUM_CIRCLES = 4;
int main()
{
    Circle circle[NUM_CIRCLES]; // 使用默认的构造函数定义
    Circle circle[NUM_CIRCLES] = {0.0, 2.0, 2.5, 10.0};//使用初始化列表,double构造函数
    Circle circle[NUM_CIRCLES] = {0.0, 2.0, 2.5};//使用初始化列表,double+默认
    Circle circle[3] = {Circle(4.0, 2, 1),Circle(2.0, 1, 3),Circle (2.5, 5, -1) };//多个参数的构造函数,则初始化项必须釆用函数调用的形式
    Circle circle [3] = { 4.0,Circle (2.0, 1, 3),2.5 };//不一定使用相同的构造函数
}

对象数组的初始化

  • 使用默认的构造函数定义
  • 列表数量足够,使用初始化列表,double构造函数
  • 列表数量不足,double+默认,不足部分使用默认构造
  • 多个参数的构造函数,则初始化项必须釆用函数调用的形式
  • 初始化列表中不一定使用相同的构造函数
  • 如果在创建对象数组时使用初始化列表,则将根据所使用参数的数量和类型为每个对象调用正确的构造函数

3.11 成员对象和封闭类‘

成员对象:一个对象是另一个类的成员变量,这个对象是成员对象

封闭类:包含成员对象的类

1 封闭类的初始化列表

封闭类的构造和析构顺序

  • 构造函数:先调用所有成员对象的构造函数,最后执行封闭类的构造函数,成员对象构造函数的执行次序和成员对象在类定义中的次序一致,与它们在构造函数初始化列表中出现的次序无关。
    • 原因:封闭类的初始化可能用到成员对象,因此成员对象必须先初始化。
  • 析构函数:先执行封闭类的析构函数,然后再执行成员对象的析构函数,成员对象析构函数的执行次序和构造函数的执行次序相反,即先构造的后析构,这是 C++ 处理此类次序问题的一般规律
    • 原因:封闭类的析构函数可能还用到成员对象,因此成员对象不能先消亡。

2 封闭类的复制构造函数

#include <iostream>
using namespace std;
class A
{
public:
    A() { cout << "default" << endl; }
    A(A &a) { cout << "copy" << endl; }
};
class B
{
    A a;
};
int main()
{
    B b1, b2(b1);
    return 0;
}
程序的输出结果是:
default
copy
  • 封闭类的对象,如果是用默认复制构造函数初始化的,那么它包含的成员对象也会用复制构造函数初始化
    • b2使用拷贝构造函数,那么A也使用拷贝构造函数。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值