C++Primer学习——第7章 类

本文深入探讨了C++中的类,包括数据抽象、封装、构造函数、默认构造函数、成员初始化、类作用域、友元函数和类的静态成员。详细阐述了如何定义和使用这些概念,强调了它们在实现对象初始化、访问控制和合作功能中的作用。此外,还讨论了可变数据成员、内联函数和const成员函数的使用,以及如何通过合成默认构造函数和显式构造函数控制对象的创建。
摘要由CSDN通过智能技术生成

文章目录

*类的基本思想是数据抽象(data abstraction)封装(encapsulation)。数据抽象是一种依赖于**接口(interface)实现(implementation)*分离的编程技术。类的接口包括用户所能执行的操作;类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。

定义改进的Sales_data类

成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。


#include <iostream>
#include <string>

struct Sales_data
{
    std::string isbn()const {return bookNo;}
    Sales_data& combine(const Sales_data&);
    
    double avg_price()const;
    
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);

定义在类内部的函数时隐式的inline函数。

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this。

在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为this所指的正是这个对象。

任何对类成员的直接访问都被看作this的隐式引用。

this是一个常量指针,我们不允许改变this中保存的地址。

const成员函数

紧随参数列表之后的const关键字,const的作用是修改隐式this指针的类型。

默认情况下,this的类型是指向类类型非常量版本的常量指针。意味着我们不能把this绑定到一个常量对象上。我们不能在一个常量对象上调用普通的成员函数。

紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数(const member function)。

常量对象,以及常量对象的引用或指针都只能调用常量成员函数。

类作用域和成员函数

编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体。成员函数体可以随意使用类中的其他成员而无须在意这些成员出现的次序。

在类的外部定义成员函数

当我们在类的外部定义成员函数时,成员函数的定义必须与它的声明匹配。如果成员函数被声明成常量成员函数,那么它的定义必须在参数列表后明确指定const属性。同时,类外部定义的成员的名字必须包含它所属的类名:

double Sales_data::avg_price() const {
    if(units_sold)
        return revenue / units_sold;
    else
        return 0;
}

定义一个返回this对象的函数

Sales_data& Sales_data::combine(const Sales_data & rhs) {
    units_sold += rhs.units_sold;
    revenue =+ rhs.revenue;
    return *this;
}

调用此函数:


total.combine(trans);

total的地址被绑定到隐式的this参数上,而rhs绑定到了trans上。

定义类相关的非成员函数

std::istream& read(std::istream& is, Sales_data& item)
{
    double price = 0;
    is >> item.bookNo >> item.units_sold >> price;
    item.revenue = price * item.units_sold;
    
    return is;
}

std::ostream& print(std::ostream& os, const Sales_data& item)
{
    os << item.isbn() << " " << item.units_sold << " "
        << item.revenue << " " << item.avg_price();
    
    return os;
}
  1. IO类属于不能被拷贝的类型,我们只能通过引用来传递它们。读取和写入的操作会改变流的内容。

构造函数

每个类都分别定义了它的对象被初始化的方式,类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数(constructor)。构造函数的任务是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。

构造函数的名字和类名相同,构造函数没有返回值;构造函数有一个(可能为空的)参数列表和一个(可能为空的)函数体。类可以包含多个构造函数,和其他重载函数差不多,不同的构造函数之间必须在参数数量或参数类型上有所区别。

构造函数不能被声明成const的。当我们创建一个类的const对象时,直到构造函数完成初始化过程,对象才能真正取得其“常量”的属性。构造函数在const对象的构造过程中可以向其写入值。

合成默认构造函数

类通过一个特殊的构造函数来控制默认初始化过程,这个函数叫做**默认构造函数(default constructor)。**默认构造函数无须任何实参。

如果我们的类没有显式地定义构造函数,那么编译器就会为我们隐式地定义一个默认构造函数。

编译器创建的构造函数又被称为合成的默认构造函数(synthesized default constructor)

这个合成的默认构造函数按照如下规则初始化类的数据成员:

  • 如果存在类内的初始值,用它来初始化成员。
  • 否则,默认初始化该成员。
某些类不能依赖于合成的默认构造函数

对于一个普通的类来说,必须定义它自己的默认构造函数:

  1. 编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。这条规则的依据是,如果一个类在某种情况下需要控制对象初始化,那么该类很可能在所有情况下都需要控制。

只有当类没有声明任何构造函数的时候,编译器才会自动地生成默认构造函数。

  1. 合成的默认构造函数可能执行错误的操作。含义内置类型或复合类型的类应该在类的内部初始化这些成员,或者定义一个自己的默认构造函数。

如果类包含有内置类型或者复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。

  1. 有的时候编译器不能为某些类合成默认的构造函数。

定义Sales_data 的构造函数

#include <string>
#include <istream>

struct Sales_data
{
    Sales_data() = default; //默认构造函数
    Sales_data(const std::string &s) : bookNo(s) {}
    Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}
    Sales_data(std::istream &);

    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data &);
    double avg_price() const;

    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};
=default的含义

在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上**=default**来要求编译器生成构造函数。=default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。如果=default在类的内部,则默认构造函数是内联的;如果它在类的外部,则成员默认情况不是内联的。

构造函数初始值列表
Sales_data(const std::string &s) : bookNo(s) {}
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}

冒号及冒号和花括号之间的代码:其中花括号定义了(空的)函数体。我们把新出现的部分称为**构造函数的初始值列表(constructor initialize list),**它负责为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开。

当某个数据成员被构造函数和初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。

在类的外部定义构造函数
Sales_data::Sales_data(std::istream &is)
{
    read(is, *this);
}

当我们在类的外部定义构造函数时,必须指明该构造函数是哪个类的成员。

尽管构造函数初始值列表是空的,但是由于执行了构造函数体,所以对象的成员仍被初始化。

没有出现在构造函数初始值列表中的成员将通过相应的类内初始值(如果存在的话)初始化,或者执行默认初始化。

拷贝、赋值和析构

某些类不能依赖于合成的版本

当类需要分配类对象之外的资源时,合成的版本常常会失败。

访问控制与封装

在C++语言中,我们使用**访问说明符(access specifiers)**加强类的封装性:

  • 定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。
  • 定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(即隐藏了)类的实现细节。
#include <string>
#include <istream>

class Sales_data
{
public:
    Sales_data() = default; //默认构造函数
    Sales_data(const std::string &s) : bookNo(s) {}
    Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}
    Sales_data(std::istream &);

    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data &);
private:
    double avg_price() const
        { return units_sold ? revenue/units_sold : 0;}
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

Sales_data::Sales_data(std::istream &is)
{
    read(is, *this);
}

作为接口的一部分,构造函数和部分成员函数紧跟在public说明符之后;而数据成员和作为实现部分的函数则跟在private说明符后面。

每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾处为止。

struct和class关键字

我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;如果我们使用class关键字,则这些成员是private的。

使用class和struct定义类唯一的区别是默认的访问权限。

友元

类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数称为它的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可:

#include <string>
#include <istream>

class Sales_data
{
//为非成员函数所做的友元声明
friend Sales_data add (const Sales_data&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
public:
    Sales_data() = default; //默认构造函数
    Sales_data(const std::string &s) : bookNo(s) {}
    Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) {}
    Sales_data(std::istream &);

    std::string isbn() const { return bookNo; }
    Sales_data &combine(const Sales_data &);
private:
    double avg_price() const
        { return units_sold ? revenue/units_sold : 0;}
    std::string bookNo;
    unsigned units_sold = 0;
    double revenue = 0.0;
};

Sales_data::Sales_data(std::istream &is)
{
    read(is, *this);
}

//类接口的非成员组成部分的声明
Sales_data add(const Sales_data&, const Sales_data&);
std::istream& read(std::istream&, Sales_data&);
std::ostream& print(std::ostream&, Sales_data&);

友元声明只出现在定义类的内部,最好在类定义开始或结束的位置集中声明友元。

封装的益处
  • 确保用户代码不会无意间破坏对象的状态。
  • 被封装的类的具体实现细节可以随时改变,而无须调整用户级别的代码。
友元的声明

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类外部)。

类成员

定义一个类型成员

除了定义数据和函数成员之外,类还可以自定义某种类型在类中的别名。由类定义的类型名字和其他成员一样存在访问限制。

#include <iostream>
#include <string>

class Screen{
public:
    //typedef std::string::size_type pos;
    using pos = std::string::size_type; //与上一句相同
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

用来定义类型的成员必须先定义后使用。类型成员通常出现在类开始的地方。

Screen类的成员函数
#include <iostream>
#include <string>

class Screen{
public:
    typedef std::string::size_type pos;
    Screen() = default; //因为Screen有另一个构造函数,所有本函数是必须的
    // cursor被其类内初始值初始化为0
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
    char get() const //隐式内联
        { return contents[cursor]; }
    inline char get(pos hit, pos wd) const; //显示内联
    Screen &move(pos r, pos c);
private:
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};
令成员作为内联函数

定义在类内部的成员函数是自动inline的。可以在类的内部把inline作为声明的一部分显示地声明成员函数,也能在类的外部用inline关键字修饰函数的定义:

inline Screen& Screen::move(pos r, pos c)
{
    pos row = r * width; //计算行的位置
    cursor = row + c; //在行内将光标移动到指定的列
    return *this; //以左值的形式返回对象
}

char Screen::get(pos r, pos c) const{
    pos row = r * width; //计算行的位置
    return contents[row + c]; //返回给定列的字符
}

最好只在类外部定义的地方说明inline,这样可以使类更容易理解。

可变数据成员

可以通过在变量的声明中加入mutable关键字做到这一点。

一个**可变数据成员(mutable data member)**永远不会是const,即使它是const对象的成员。一个const成员函数可以改变一个可变成员的值。

#include <iostream>
#include <string>

class Screen{
public:
    void some_member() const;
    typedef std::string::size_type pos;
    Screen() = default; //因为Screen有另一个构造函数,所有本函数是必须的
    // cursor被其类内初始值初始化为0
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
    char get() const //隐式内联
        { return contents[cursor]; }
    inline char get(pos hit, pos wd) const; //显示内联
    Screen &move(pos r, pos c);
private:
    mutable size_t access_ctr; //即使在一个const对象内也能被修改
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

void Screen::some_member() const
{
    ++access_ctr;
}

inline Screen& Screen::move(pos r, pos c) //可以在函数的定义处指定inline
{
    pos row = r * width; //计算行的位置
    cursor = row + c; //在行内将光标移动到指定的列
    return *this; //以左值的形式返回对象
}

char Screen::get(pos r, pos c) const{
    pos row = r * width; //计算行的位置
    return contents[row + c]; //返回给定列的字符
}

尽管some_member是一个const成员函数,它仍然能够改变access_ctr的值。该成员是个可变成员,因此任何成员函数,包括const函数在内都能改变它的值。

类数据成员的初始值

#include <iostream>
#include <string>
#include <vector>
#include "Screen.h"

class Window_mgr{
private:
    //这个Window_mgr追踪的Screen
    //默认情况下,一个Window_mgr包含一个标准尺寸的空白Screen
    std::vector<Screen> screens{Screen(24, 80, ' ')};
};

类内初始值必须使用=的初始化或者花括号括起来的直接初始化形式。

返回*this的成员函数


class Screen{
public:
    using pos = std::string::size_type;
    Screen& set(char);
    Screen& set(pos, pos, char);
    void some_member() const;
    typedef std::string::size_type pos;
    Screen() = default; //因为Screen有另一个构造函数,所有本函数是必须的
    // cursor被其类内初始值初始化为0
    Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
    char get() const //隐式内联
        { return contents[cursor]; }
    inline char get(pos hit, pos wd) const; //显示内联
    Screen &move(pos r, pos c);
private:
    mutable size_t access_ctr; //即使在一个const对象内也能被修改
    pos cursor = 0;
    pos height = 0, width = 0;
    std::string contents;
};

inline Screen& Screen::set(char c)
{
    contents[cursor] = c; //设置当前光标所在位置的新值
    return *this;
}

inline Screen& Screen::set(pos r, pos col, char ch)
{
    contents[r * width + col] = ch;
    return *this;
}

void Screen::some_member() const
{
    ++access_ctr;
}

inline Screen& Screen::move(pos r, pos c) //可以在函数的定义处指定inline
{
    pos row = r * width; //计算行的位置
    cursor = row + c; //在行内将光标移动到指定的列
    return *this; //以左值的形式返回对象
}

char Screen::get(pos r, pos c) const{
    pos row = r * width; //计算行的位置
    return contents[row + c]; //返回给定列的字符
}

从const成员函数返回*this

this将是一个指向const的指针而*this是const对象。

一个const成员函数如以引用的形式返回*this,那么它的返回类型将是常量引用。

基于const的重载

通过区分成员函数是否是const的,我们可以对其进行重载。

非常量版本的函数对于常量对象是不可用的,只能在一个常量对像上调用const成员函数。虽然可以在非常量对象上调用常量版本或非常量版本,但是非常量版本是一个更好地匹配。


class screen{
public:
	//根据对象是否是const重载了display函数
	Screen& display(std::ostream&)
		{ do_display(os);
		return *this; }
	const Screen& display(std::ostream& os) const
	{
		do_display(os);
		return *this;
	}
private:
	void do_display(std::ostream& os) const
	{
		os << contents;
	}
};

类类型

每个类定义了唯一的类型。对于两个类来说,即使它们的成员完全一样,这两个类也是完全不同的类。


struct First{
	int memi;
	int getMem();
};
struct Second{
	int memi;
	int getMem();
};
First obj1;
Second obj2 = obj1; //错误,两个类不是相同类型

即使两个类的成员列表完全一致,它们也是不同的类型。对于同一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事儿。


Sales_data item1; //默认初始化Sales_data 类型的对象
class Sales_data item1; //一条等价的声明

类的声明

我们仅仅声明类而暂不定义它:


class Screen; //Screen类的声明

这种声明被称作前向声明(forward declaration),它向程序引入了名字Screen并且指明它是一种类类型。对于类型Screen来说,在它声明之后定义之前是一个不完全类型(incomplete type),也就是说,此时我们已知Screen是一个类类型,但是不清楚它到底包含哪些成员。

不完全类型只能在非常有限的情境下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回类型的函数。

对于一个类来说,在我们创建它的对象之前该类必须被定义过,而不能仅仅被声明。

类也必须首先被定义过,然后才能用引用或者指针访问其成员。

必须首先完成类的定义,然后编译器才能知道存储该数据成员需要多少空间。因为只有当类全部完成后类才算被定义,所有一个类的成员类型不能是该类自己。一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针:


class Link_screen{
	Screen window;
	Link_Screen *next;
	Link_screen *prev;
};

友元再探

如果一个类指定了友元类,则友元类的成员函数可以访问此类包括非公有成员在内的所有成员。


class Window_mgr{
public:
    //窗口中每个屏幕的编号
    using ScreenIndex = std::vector<Screen>::size_type;
    //按照编号将指定的Screen重置为空白
    void clear(ScreenIndex);
private:
    std::vector<Screen> screens{Screen(24, 80, ' ')};
};

void Window_mgr::clear(ScreenIndex i) {
    //s是一个Screen的引用,指向我们想清空的那个屏幕
    Screen& s = screens[i];
    //将那个选定的Screen重置为空白
    s.contents = std::string(s.height * s.width, ' ');
}

友元不存在传递性。每个类负责控制自己的友元类或友元函数。

令成员函数作为友元

当把一个成员函数声明为友元时,我们必须明确指出该成员函数属于哪个类:


class Screen{
	//Window_mgr::clear必须在Screen类之前被声明
	friend void Window_mgr::clear(ScreenIndex);
};

函数重载和友元

如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明:


//重载的storeOn函数
extern std::ostream& storeOn(std::ostream&, Screen&);
extern BitMap& storeOn(BitMap&, Screen&);
class Screen{
	friend std::ostream& storeOn(std::ostream&, Screen&);
};

友元声明和作用域

类和非成员函数的声明不是必须在它们的友元声明之前。当一个名字第一次出现在一个友元声明中时,我们隐式地假定该名字在当前作用域是可见的。

甚至就算在类的内部定义该函数,我们也必须在类的外部提供相应的声明从而使得函数可见。即使我们仅仅是用声明友元的类的成员调用该友元函数,它也必须是被声明过的:


struct x{
	friend void f() { // 友元函数可以定义在类的内部 }
	x(){f();} //错误,f还没有被声明.
	void g();
	void h();
};

void x::g() {return f();} //错误,f还没有被声明
void f();
void x::h(){ return f(); } //正确,现在f的声明在作用域中了

类的作用域

每个类都会定义它自己的作用域。在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问。对于类类型成员则使用作用域运算符访问。跟在运算符之后的名字都必须是对应类的成员:


Screen::pos ht = 24, wd = 80;
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get();
c = p->get();

在类的外部,成员的名字会被隐藏起来!!!!!

名字查找与类的作用域

**名字查找(name lookup) ** (查找与所用名字最匹配的声明的过程):

  • 首先,在名字所在的块中寻址其声明语句,只考虑在名字的使用之前出现的声明。
  • 如果没找到,继续查找外层作用域。
  • 如果最终没有找到匹配的声明,则程序报错。

类的定义分两步处理:

  • 首先,编译成员的声明。
  • 直到类全部可见后才编译函数体。

编译器处理完类中的全部声明后才会处理函数成员的定义。

因为成员函数体直到整个类可见后才会被处理,所有它能使用类中定义的任何名字。

用于类成员声明的名字查找

声明中使用的名字,包括返回类型或者参数列表中使用的名字,都必须在使用前确保可见。如果某个成员的声明使用了类尚未出现的名字,则编译器将会在定义该类的作用域中继续查找。

类型名要特殊处理

一般来说,内存作用域可以重新定义外层作用域的名字,即使该名字已经在内存作用域中使用过。然而在类中,如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字:


typedef double Money;
class Account{
public:
	Money balance() { return bal; } //使用外层作用域的Money
private:
	typedef double Money; //错误不能重新定义Money
	Money bal;
};

类型名的定义通常出现在类的开始处,这样就能确保所有使用该类型的成员都出现在类名的定义之后。

成员定义中的普通块作用域的名字查找

成员函数中使用的名字按照如下方式解析:

  • 首先,在成员函数内查找改名字的声明。只要在函数使用之前出现的声明才会被考虑。
  • 如果在成员函数内没有找到,则在类内继续查找,这时类的所有成员函数都会被考虑到。
  • 如果类内也没找到该名字的声明,在成员函数定义之前的作用域内继续查找。

一般来说,不建议使用其他成员的名字作为某个成员函数的参数。

尽管类的成员被隐藏了,但我们仍然可以通过加上类的名字或显示地使用this指针来强制访问成员。

类作用域之后,在外围的作用域中查找

如果编译器在函数和类的作用域中都没有找到名字,它将接着在外围的作用域中查找。如果我们需要的是外层作用域中的名字,可以显式地通过作用域运算符来进行请求:


void Screen::dummy_fcn(pos height){
	cursor = width * ::height; //这里用的是全局作用域中的height
}

尽管外层的对象被隐藏掉了,但我们仍然可以用作用域运算符访问它。

构造函数再探

构造函数初始值列表

如果没有在构造函数的初始值列表中显式地初始化成员,则该成员将在构造函数函数体之前执行默认初始化。


Sales_data::Sales_data(const std::string& s, unsigned cnt, double price)
{
	bookNo = s;
	units_sold = cnt;
	revenue = cnt * price;
}

构造函数的初始值有时必不可少

如果成员是const或者是引用的话,必须将其初始化。当成员属于某种类类型且该类型没有定义默认构造函数,也必须为这个成员初始化。

我们初始化const或者引用类型的数据成员的唯一机会是通过构造函数初始值。

如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。

建议:使用构造函数初始值

在很多类中,初始化和赋值的区别事关底层效率问题:前者直接初始化数据成员后者则先初始化再赋值。除了效率问题外更重要的是:一些数据成员必须被初始化。

成员初始化的顺序

构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。

成员的初始化顺序与它们在类定义中的出现顺序一致:第一个成员先被初始化,然后第二个,以此类推····· 构造函数初始值列表中初始值的前后位置关系不会影响实际的初始化顺序。

最好令构造函数初始值的顺序与成员声明的顺序保持一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。

如果可能,最好用构造函数的参数作为成员的初始值,而尽量避免使用同一个对象的其他成员。

默认实参和构造函数

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认的构造函数。

委托构造函数

C++11新标准扩展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数(delegating constructor)。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或全部)职责委托给了其他构造函数。

一个委托构造函数也有一个成员初始值的列表和一个函数体。在委托构造函数内,成员初始值列表只有一个唯一的入口,就是类名本身。和其他成员初始值一样,类名后面紧跟圆括号括起来的参数列表,参数列表必须与类中另外一个构造函数匹配。


class Sales_data{
public:
    //非委托构造函数使用对应的实参初始化成员
    Sales_data(std::string a, unsigned cnt, double price):
        bookNo(a), units_sold(cnt), revenue(cnt * price) {}
    //其余构造函数全部委托给另一个构造函数
    Sales_data() : Sales_data("", 0, 0) {}
    Sales_data(std::string s) : Sales_data(s, 0, 0) {}
    Sales_data(std::istream& is) : Sales_data() 
    { read(is, *this); }
};

当一个构造函数被委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。假如函数体包含有代码的话,将先执行这些代码,然后控制权才会交还给委托者的函数体。

默认构造函数的作用

当对象被默认初始化或值初始化时自动执行默认构造函数。默认初始化在以下情况下发生:

  • 当我们在块作用域内不使用任何初始值定义一个非静态变量或者数组时。
  • 当一个类本身含有类类型的成员且使用合成的默认构造函数时。
  • 当类类型成员没有在构造函数初始值列表中显式地初始化时。

值初始化在以下情况下发生:

  • 在数组初始化的过程中,如果我们提供的初始值数量少于数组的大小时。
  • 当我们不使用初始值定义一个局部静态变量时。
  • 当我们通过书写形如T()表达式显式地请求值初始化时,其中T是类型名。

在实际中,如果定义了其他构造函数,那么最好也提供一个默认构造函数。

使用默认构造函数

如果想定义一个使用默认构造函数进行初始化的对象,正确的方法是去掉对象名之后的空的括号对:


Sales_data obj; //obj是个默认初始化的对象
Sales_data obj (); //obj是一个函数,不是一个默认初始化的类对象

隐式的类类型转换

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数(converting constructor)

编译器只会自动地执行一步类型转换!!!!

抑制构造函数定义的隐式转换

在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止:


class Sales_data{
public:
    Sales_data() = default;
    Sales_data(const std::string& s, unsigned n, double p):
        bookNo(s), units_sold(n), revenue( p * n ) {}
    explicit  Sales_data(const std::string &s) : bookNo(s) {}
    explicit Sales_data(std::istream&);
};

关键字explicit只对一个实参的构造函数有效。需要多个实参的构造函数不能用于执行隐式转换,所以无须将这些构造函数指定为explicit的。只能在类内声明构造函数时,使用explicit关键字,在类外定义时不应该重复!!!!

explicit构造函数只能用于直接初始化

发生隐式转换的一种情况是当我们执行拷贝形式的初始化时(=)。我们只能使用直接初始化而不能使用explicit构造函数。

当我们用explicit关键字声明构造函数时,它将只能以直接初始化的形式使用。而且,编译器将不会在自动转换过程中使用该构造函数。

标准库中含有显示构造函数的类
  • 接受一个单参数的const char * 的string构造函数不是explicit的。
  • 接受一个容量参数的vector构造函数是explicit的。

聚合类

**聚合类(aggregate class)**使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:

  • 所有成员都是public的。
  • 没有定义任何构造函数。
  • 没有类内初始值。
  • 没有基类,也没有virtual函数。

struct Data{
	int ival;
	string s;
};

我们可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员:


Data val2 = { "Anna", 1024 };

初始值的顺序必须与声明的顺序一致,如果初始值列表中的元素个数少于类的成员数量,则靠后的成员被值初始化。初始值列表的元素个数绝对不能超过类的成员数量。

字面值常量类

数据成员都是字面值类型的聚合类是字面值常量类。如果一个类不是聚合类,但它符合下述要求,则它也是一个字面值常量类:

  • 数据成员必须是字面值类型。
  • 类必须至少含有一个constexpr构造函数。
  • 如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。
  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

constexpr构造函数

构造函数不能是const的!!!!

constexpr构造函数可以声明成=default的形式(或者是删除函数的形式)。否则,constexpr构造函数就必须既符合构造函数的要求(意味着不能包含返回语句),有符合constexpr该函数的要求(意味着它能拥有唯一可执行语句就是返回语句)。综合这两点,constexpr构造函数的函数体一般来说是空的。


class Debug{
public:
    constexpr Debug(bool b = true) : hw(b), io(b), other(b) {}
    constexpr Debug(bool h, bool i, bool o) :
            hw (h), io(i), other(o) { }
    constexpr bool any() {return hw || io || other; }\
    void set_io(bool b) { io = b; }
    void set_hw(bool b) { hw = b; }
    void set_other(bool b) { other = b; }
private:
    bool hw;
    bool io;
    bool other;
};

constexpr构造函数必须初始化所有数据成员,初始值或者使用constexpr构造函数或者是一条常量表达式。

constexpr构造函数用于生成constexpr对象以及constexpr函数的参数或返回类型。

类的静态成员

声明静态成员

我们通过在成员的声明之前加上关键字static使得其与类关联在一起。静态成员可以是public的或private的。静态数据成员的类型可以是常量、引用、指针、类类型等等。


class Account{
public:
    void calculate(){
        amount += amount * interestRate;
    }
    static double rate(){
        return interestRate;
    }

    static void rate(double);

private:
    std::string owner;
    double amount;
    static double interestRate;
    static double initRate();
};

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。

类的静态成员函数也不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的,而且我们也不能在static函数体内使用this指针。

使用类的静态成员

我们使用作用域运算符直接访问静态成员:


double r;
r = Account::rate(); //使用作用域运算符访问静态成员

虽然静态成员不属于类的某个对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员:


Account ac1;
Account *ac2 = &ac1;
// 调用静态成员函数rate的等价形式
r = ac1.rate(); //通过Account的对象或引用
r = ac2->rate(); //通过指向Accounr的指针

成员函数不用通过作用域运算符就能直接使用静态成员:


class Account{
public:
    void calculate(){
        amount += amount * interestRate;
    }
    
private:
    static double interestRate;
    //其他成员与之前的版本一致
};

定义静态成员

当在类的外部定义静态成员时,不能重复使用static关键字,该关键字只出现在类内部的声明语句:


void Account::rate(double newRate){
	interestRate = newRate;
}

和类的所有成员一样,当我们指向类外部的静态成员时,必须指明成员所属的类名。static关键字则只出现在类内部的声明语句中。

静态数据成员不属于类的任何一个对象,所有它们并不是在创建类的对象时被定义的。它们不是由构造函数初始化的。一般来说,我们不能在类的内部初始化静态成员,必须在类的外部定义和初始化每个静态成员。一个静态成员只能定义一次。

静态数据成员定义在任何函数之外,因此一旦它被定义,就将一直存在于程序的整个生命周期中。

要想确保对象只定义一次,最好的办法是把静态数据成员的定义与其他非内联函数的定义放在同一个文件中。

静态成员的类内初始化

通常情况下,类的静态成员不应该在类的内部初始化。我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式。


class Account{
public:
    static double rate(){ return interestRate; }
    static void rate(double);
private:
    static constexpr int preiod = 30; //preiod是常量表达式
    double daily_tbl[preiod];
};

即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员。


//一个不带初始值的静态成员的定义
constexpr int Account::period; //初始值在类的定义内提供

1. 静态数据成员可以是不完全类型(只声明没有定义的类型。) ;非静态成员只能声明成它所属类的指针和引用。
2. 我们可以使用静态成员作为默认实参,非静态数据成员不能作为默认实参。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值