C++ Primer笔记7---chapter7 类

1.成员函数的声明必须在类的内部,而其定义可内可外,定义在类内部分函数时隐式inline函数。对于接口组成部分的非成员函数(往往或使用到私有成员变量,可声明为友元)的定义和声明都放在类的外部。

2.this是一个指向类类型非常量版本指针常量(书中称为常量指针,不可改变其存储的地址值),在默认情况下我们不能将其绑定到一个常量对象上,这就使得我们不能再一个常量对象上调用普通的成员函数,因此我们可以将this声明为指向常量的常量指针(在成员函数的参数列表后加上const),这样只要函数体中不需要改变this所指的对象,无论是常量对象还是非常量对象都能够调用此成员函数;对这种成员函数我们称为常量成员函数

//参数列表之后加上const,表示为常量成员函数,常量对象也能够调用此成员函数(函数体中不修改此对象)
std::string isbn() const {return this->bookNo;}

3.对于返回调用者对象的成员函数,我们可以使用return *this来表示,注意返回值需要是引用,才是真正的返回调用者对象

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

class Screen{
    public:
        Screen &set(char);
        Screen &set(pos,pos,char);
        //xxx
};

//返回的是引用类型,才是真正的返回调用者对象,否则只是返回的拷贝
inline Screen &Screen::set(char c){
    content[cursor]=c;
    return *this;
}

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

 

4.构造函数没有返回类型,也不能被声明为const的,构造函数在对象的构造过程中向其const成员写值,但注意是初始化,而非等号赋值;如果成员是常量或者引用的话,必须将其初始化

原因:随着构造函数一开始执行,初始化就完成了。我们初始化const或者引用类型的数据成员的唯一机会就是通过构造函数的初始值

class ConstRef{
public:
    ConstRef(int ii);
private:
    int i;
    const int ci;
    int &ri;        
}

//错误:企图使用赋值的方式来给值
ConstRef::ConstRef(int ii)
{
    //赋值
    i=ii;    //正确
    ci =ii;  //错误:不能给const赋值
    ri = i;  //错误:ri没有初始化
}

//正确:显示的初始化引用和const成员
ConstRef::ConstRef(int ii):i(ii),ci(ii),ri(i){}

5.我们一旦定义了一些其他的构造函数,除非我们再定义一个默认的构造函数,否则将没有默认构造函数,在我们定义了自己的构造函数后,我们往往需要自己写上默认构造函数,构造默认函数可如下法;

构造函数初始值列表可如下法;

如果是在函数体内使用赋值符号来初始化,那么实际上是赋值而非初始化,在构造const类型对象时,会发生错误,而是用初始值列表不会有问题;

构造函数可以在类外面定义,但是仍要在类里面声明

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

6.class和struct的区别:使用struct关键字,则定义在第一个访问说明符之前的成员的是public;如果使用class关键字,则这些成员是private的;除此之外毫无区别

7.友元:类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为他的友元

class Sales_data{
    //为Sales_data的非成员函数所做的友元声明
    friend Sales_data add(const Sales_data&, const Sales_data);
    friend std::istream &read(str::istream &, Sales_data &);
    friend std::ostream &print(str::ostream&, const Sales_data&);
    //friend class xxx   也可以声明某个类为友元,这样xxx类的成员可以访问Sales_data的私有部分
    //friend xxx::xxx    也可以声明其他类的某个成员函数为友元
    //xxx


};

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

友元声明只能出现在类定义的内部,一般来说最好的在类定义的开始或者结束位置集中声明;友元关系不存在传递性

友元的声明仅仅指定了访问权限,而非一个通常意义上的声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。通常把友元声明和类本身放在同一个头文件中,使得友元对类用户可见

在令A的成员函数X为B的友元时,需要遵循一定的设计方式:首先定义类A并声明函数X但是不能定义他,接下来定义B并声明对于A::X的友元,最后定义X

8.类还可以有类型成员,一般放在类的开始部分

class Screen{
    public:
        //定义一个类型成员,可以隐藏pos的实际类型
        typedef std:string::size_type pos;
    private:
        pos cursor = 0;
        pos height=0,width =0;
        std::string contents; 

}

9.可变数据成员:永远不会是const,即使他是const对象的成员,也仍然可以改变,可以使用mutable来声明

10.基于const的重载:非常量版本的函数对于常量对象是不可用的,所以我们在一个常量对象上只能调用const成员函数;另一方面,虽然可以在非常量对象上调用常量版本或者非常量版本,但显然此时非常量版本是一个更好的选择

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

        const Screen &display(std::ostream &os)const
            {do_display(os);return *this}


    private:
        //该函数负责显示Screen的内容
        void do_display(std::ostream &os)const {os<<contents};
};

当我们在耨个对象上调用display时,对象是否为const决定了应该调用那个版本的display函数

11.类的作用域:一旦遇到了类名,定义的剩余部分就在类的作用域之内了,这里的剩余部分包括参数列表和函数体

//在处理参数列表前就已经提到了window_mgr,所以不用再次说明ScreenIndex处于window_mgr之中
void window_mgr::clear(ScreenIndex i){
    Screen &s=sreens[i];
    s.contents = string(s.height * s.width,' ');
}

另一方面,函数的返回类型通常出现在函数名前,因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外,此时返回值类型必须指定是那个类的成员,此时函数名仍然要加上类名(定义在类的外部,而且不属于“剩余部分”)

12.名字查找:首先在名字所在的块中查找,如果没找到继续查找外层作用域,如果最终没找到匹配的声明则程序报错。

对于类内部的成员函数来说:首先编译成员的声明,直到类全部可见之后才编译函数体(意味着在类的内部,你可以先在函数体中使用目前尚未定义的成员变量)

13.成员初始顺序:成员的初始化顺序与他们在类定义中出现的顺序一致

class X{
    int i;
    int j;

public:
    //未定义错误:i在j之前被初始化
    X(int val):j(val),i(j){ }

    //最好用构造函数的参数作为成员的初始值
    X(int val):j(val),i(val){ }
    
}

最好令构造函数初始值顺序与成员声明的顺序保持一致

14.构造函数也可以有默认实参,当一个构造函数为所有的参数都提供默认实参,则他实际也就定义了默认构造函数(一个实参不传都可以)

15.委托构造函数:一个委托构造函数使用它所属的类的其他构造函数执行它自己的初始化过程(就是一个构造函数调用其他的重载构造函数)

class Sales_data{
public:
    //非委托构造函数
    Sales_data(std::string s, unsigned cnt, double price):
        bookNo(s), 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);}

}

16.隐式的类类型转换:如果构造函数只有一个实参,那么这个构造函数实际上定义了从“这个类型的参数”到“此类类型”的隐式转化机制,但是注意只允许一步类类型转换

//比如构造函数Sales_data::Sales_data(string);
//顺便用上Sales_data &Sales_data::combine(Sales_data)作为例子

string null_book = "9-999-9999-9";
//item.combine实际接受的参数类型为const Sales_data&,实际上编译器根据构造函数自动的将string类型的null_book转换为了Sales_data类型的临时对象,并将其作为参数传入combine函数
item.combine(null_book);


//只允许一步的类类型转换
item.combine("9-999-9999-9");    //错误,这个转换涉及两步:一是将"9-999-9999-9转换为"string对象,二是将string对象转换为Sales_data对象


//正确:显示的转换为string,隐式的转换为Sales_data
item.combine(string("9-999-9999-9"));
//正确:隐式的转换为string,显示的转换为Sales_data
item.combine(Sales_data("9-999-9999-9"));

抑制构造函数定义的隐式转换:将构造函数声明为explicit(只对有一个实参的构造函数起作用,需要多个实参的构造函数不能用于执行隐式转换),explicit构造函数只能用于直接初始化(使用括号进行初始化)而不能用拷贝形式初始化(使用等号,使用等号也是发生隐式转换的一种情形);但是可以用构造函数或者static_cast进行显示的转换

Sales_data items(null_book);    //正确:直接初始化
Sales_data item2 =null_book;    //错误:不能将explicit构造函数用于拷贝形式的初始化过程

17.聚合类&字面值常量类

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

//聚合类
struct Data{
    int ival;
    string s;
};

//使用花括号来初始化,注意初始化顺序不能错
Data vall={0,"Anna"};

17.2 字面值常量类:数据成员都是字面值类型的聚合类是字面值常量类,或者不是聚合类但是符合以下要求:

1)数据成员必须都是字面值类型的聚合类

2)类必须至少含有一个constexpr构造函数

3)如果一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;或者如果成员属于某种类类型,则成员有自己的constexpr构造函数

4)类必须使用析构函数的默认定义,该成员负责销毁类的对象

constexpr构造函数可以声明为default,constexpr构造函数(构造函数不能有返回值,constexpr函数能拥有的唯一可执行语句就是返回语句)的函数体一般来说是空的

constexpr构造函数必须初始化所有数据成员

18.类的静态成员:一些成员函数只与类本身相关,而不与类的各个对象保持关联,且此成员为所有的对象所共享。类似的,静态成员函数也不与任何对象绑定在一起,他们不包含this指针,因此静态成员不能声明为const(常量成员函数就是为了将*this也就是对象声明为常量,而这里没有this,因此无法声明),而我们也不能在static函数体中使用this指针。

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

18.2 static不属于类的任何一个对象,因此不是由构造函数初始化的,必须在类的外部定义和初始化每个静态成员

18.3 要想确保对象(这里即使静态数据成员)只定义一次,最好把静态数据成员的定义与其他非内联函数的定义放在同一个文件中去

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

//可以直接是用作用域运算符访问
double r;
r=Account::rate();

//可以通过对象以及对象的指针访问
Account ac1;
Account *ac2=&ac1;
r=ac1.rate();    //等价
r=ac2->rate();   //等价

//在类的外部定义静态成员,不能重复static关键字
void Account::rate(double newRate){
    interestRate = newRate;
}

//必须在类的外部定义和初始化静态数据成员,从类名开始,这条语句的剩余部分就位于类的作用域之内,因此initRate不用再加上类名了
double Account::inerestRate = initRate();

 18.4 通常情况下,类的静态成员不应该在类内初始化,但是我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。一般即使一个常量静态数据成员在类的内部被初始化了,通常情况下也应该在类的外部定义一下该成员,否则对程序的维系改动就容易犯错误。

class Account{
xxxxxxx.xxxxxxxxxxxx
private:
    static constexpr int period =30;    //period是常量表达式
    double daily_tbl[period];

xxxxxxxxxxxx.xxxxxxx
};

//如果类的内部提供了一个初始值,则成员的定义不能再定一个初始值
constexpr int Account::period;

 18.5 静态数据成员的类型可以使他所属的类类型,但是非静态数据成员则只能声明成它所属的类的指针或者引用。

 

19.

我永远不知道我每次看书的时候在想什么,5年过去了,还是一样的菜。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值