Cpp阅读笔记-类

类的基本概念

首先是一段自定义类的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include<bits/stdc++.h>
using namespace std;
class Screen{
public:
typedef std::string::size_type pos;
int sit=0;
Screen()= default;
//screen() {}
Screen(pos ht,pos wd,char c):height(ht),width(ht),contents(ht*wd,c),sit(0){};
char get()const{return contents[cursor];}
inline char get(pos ht,pos wd)const;
int getH() {
return cursor;
}
string pr()const;
Screen &move(pos r,pos c);
void move(pos r,pos c,int sit);
Screen &set(char);
Screen &set(pos,pos,char);
Screen &display(ostream &os){
do_display(os);return *this;
}
const Screen &display(ostream &os)const{
do_display(os);return *this;
}
friend class window_mgr;
//friend void window_mgr::clear(Screenindex);
private:
pos cursor=0;pos height=0,width=0;
string contents;
void do_display(ostream &os)const{
os<<contents<<endl;
}
};
Screen &Screen::move(pos r,pos c)
{
pos row=r*width;
cursor=row+c;
return *this;
}
void Screen::move(pos r,pos c,int sit)
{
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];
}
Screen &Screen::set(char c)
{
contents[cursor]=c;
return *this;
}
Screen &Screen::set(pos r,pos col,char ch)
{
contents[r*width+col]=ch;
return *this;
}
string Screen::pr() const {
return contents;
}
class window_mgr{
public:
using Screenindex=vector<Screen>::size_type;
void clear(Screenindex);
Screenindex addscreen(const Screen&);
private:
vector<Screen>screens{Screen(10,20,' ')};
};
void window_mgr::clear(window_mgr::Screenindex i)
{
Screen &s=screens[i];
s.contents.clear();
}
window_mgr::Screenindex window_mgr::addscreen(const Screen &s)
{
screens.push_back(s);
return screens.size()-1;
}

int main()
{
int x=10;
Screen screen1;
Screen screen2(10, 10, 'a');screen2.sit=3;
cout<<screen1.sit<<endl;
//screen1.move(2,3);
screen1.move(2,3,1);
screen1.move(3,4).sit=screen2.sit;
// cout << x << " " << screen1.getH() << endl;
// cout<<screen1.sit<<endl;
screen2.set(5,5,'d');
// cout<<screen2.get(5,5)<<endl;
// cout<<screen2.pr()<<endl;

screen2 .display(cout);
window_mgr w1;
// w1.screens[0].display(cout);
w1.clear(0);

return 0;
}

成员函数(类似Screen1.set()这种)的声明必须在类的内部,定义则可以在类内部也可以在类外部。而接口组成部分的函数或者说只是使用了类的函数,比如说用两个类作为自己的参数的函数,声明和定义都在类外部

this

实例函数:
std::string isbn() const{return bookNo;}

成员函数通过名为this的隐式参数来访问调用它的那个对象。例如,如果调用total.isbn(),编译器会把total的地址传递给isbn的隐式形参this,相当于
Sales_data::isbn(&total)

this形参是隐式定义的,任何自定义名为this的参数或变量的行为都是非法的,我们可以在成员函数体内部使用this,虽然没有必要。例:isbn还能如下定义:
std::string isbn() const{return this->bookNo;}

isbn函数参数列表后的const的作用是修改this指针的类型,默认情况下,this的类型是指向类类型的非常量版本常量指针。这就意味着在默认情况下我们不能把this绑定在一个常量对象上。

类作用域和成员函数

编译器处理类时, 首先编译成员声明,然后才轮到成员函数体(如果有的话)因此,成员函数体可以随意使用类中的其他成员函数而不必在意这些成员出现的先后顺序(即使出现在函数声明之后也没有问题)。

作用域运算符:类似于Sales_data::avg_price这样的::,说明了以下事实:
有一个名为avg_price的函数,并且该函数被声明在类Sales_data的作用域内。

关于函数引用IO类型作为参数

IO类属于不能被拷贝的类型,因此只能通过引用来传递他们。

构造函数

构造函数的名称与类的名称相同
只要类的对象被创造,就会执行构造函数。注意:构造函数没有返回类型
一个类可以包含多个构造函数,和其他重载函数类似。但是,构造函数不能被声明成const的。

如果一个类没有显式定义构造函数,那么编译器就会隐式地指定一个默认构造函数(即合成的默认构造函数),一般来说,容器都为空,数据都为0,bool都为false。在C++11中,如果需要默认行为,可以在参数列表后写上=default来要求编译器生成默认构造函数

构造函数举例:

1
2
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){}

冒号与花括号之间的部分称为构造函数初始值列表,它负责为新创建对象的一个或者几个数据成员赋值。如果有数据成员被初始化列表所忽略,那么它将被以与默认构造函数相同的方式隐式初始化。

注意:上面的函数中的函数体中还可以执行其他的任务或空置。

一般来说,初始化函数中成员初始化的顺序没什么关系,但是如果涉及到用一个成员去初始化另一个成员时,就有关系了。比如说:

1
2
3
4
5
6
class X{
int i;
int j;
public:
X(int val):j(val),i(j){}//这是错误的,因为i在j之前被初始化
};

在上面这个类的构造函数中,i先被初始化,j接着才被初始化,因此会出错!

拷贝,赋值和析构

通俗来说,一个类的对象可以直接被赋予另一个对象的值,例如total=trans,其中total跟trans都是类对象。但是,管理动态内存的类通常不能这么做
不过使用vector跟string的类并不受此影响。

访问控制与封装

访问说明符:public与private
定义在private之后的成员可以被类的成员函数以及友元访问,但不能被使用该类的函数访问。注意,struct默认为public,class默认为private

友元

类可以把其他的类定义为友元,也可以把其他类的成员函数定义为友元;
类可以允许其他类或者函数访问他的private成员,只要给其他类或者函数加上friend(友元)的声明即可。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Screen{
public:
typedef std::string::size_type pos;
int sit=0;
Screen()= default;
//screen() {}
Screen(pos ht,pos wd,char c):height(ht),width(ht),contents(ht*wd,c),sit(0){};
char get()const{return contents[cursor];}
inline char get(pos ht,pos wd)const;
int getH() {
return cursor;
}
friend class window_mgr;//将window_mgr声明为Screen的友元
//friend void window_mgr::clear(Screenindex);
private:
pos cursor=0;pos height=0,width=0;
string contents;
void do_display(ostream &os)const{
os<<contents<<endl;
}
};
class window_mgr{
public:
using Screenindex=vector<Screen>::size_type;
void clear(Screenindex);
Screenindex addscreen(const Screen&);
private:
vector<Screen>screens{Screen(10,20,' ')};
};

这样一来,window_mgr就可以访问Screen类的私有数据成员了。
友元声明只能出现在类的内部,但是类内部出现的具体位置不限
友元的声明仅仅提供了一个访问权限,要想真的使用这个函数,在类的外部需要再专门声明一次
友元关系不具有传递性
如果将一个其他类的成员函数声明为当前类的友元函数,那么这个函数必须已经被定义;但是如果是将其他一个类声明为友元,那么这个类只需要被声明即可
如果把一组重载函数声明为友元,那么每一个函数都必须声明一次
友元函数可以定义在类内部,但是必须在类的外部声明使得函数可见

类的其他特性(类成员)

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

和非成员函数一样,成员函数也可以被重载,只要函数之间在参数数量或者类型上有所区别即可。

类数据成员的初始值

可以给类的数据成员添加一个默认的初始值

1
2
3
4
5
6
7
8
class window_mgr{
public:
using Screenindex=vector<Screen>::size_type;
void clear(Screenindex);
Screenindex addscreen(const Screen&);
private:
vector<Screen>screens{Screen(10,20,' ')};
};

返回*this的成员函数

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

返回引用的函数是左值的,(通过函数名前面的&符号来体现)意味着这些函数返回的是对象本身而不是对象的副本;如果这个函数不是左值的,那么返回的数据只是函数中调用对象的一个copy;
举个例子,如果有这么一系列操作:myscreen.move(4,0).set('#');
这些操作会在同一个对象上执行,因为move()set()函数都是左值的;
也就是说,这个函数等价于myscreen.move(4,0);myscreen.set('#');

如果move()set()函数不是左值的,那么上述语句将会变成:
Screen temp=myscreen.move(4,0);
temp,set('#');
也就是说,第2步并不会改变原来myscreen中的contents(内容);

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

基于const的重载

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

1
2
3
4
5
6
Screen &display(ostream &os){
do_display(os);return *this;
}
const Screen &display(ostream &os)const{
do_display(os);return *this;
}

当我们在某个对象上调用display函数时,该对象是否是const决定了应该调用display的哪个版本;
例如:

1
2
3
4
Screen myscreen(5,3);
const Screen blank(5,3);
myscreen.set('#').display(cout);//调用非常量版本
blank.display(cout);//调用常量版本

类类型

注意:即使两个类的成员列表完全一致,它们也是不同的类型。对于一个类而言,它的成员和其他任何类的成员都是不一样的。

类的声明

我们可以仅声明类而暂时不定义它;这就是所谓的前向声明,在声明之后定义之前的类型被称为不完全类型

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

对于一个类而言,我们在创造它的对象之前必须将其定义(不然编译器哪知道怎么初始化233)。但是一个类一旦名字出现后,它就被认为是声明过了,因此类允许包含指向它自身类型的引用或指针。

类的作用域

对于一条函数定义,一旦遇到了类名,定义的剩余部分就在类的作用域之内了。因此在参数列表和函数体内就可以直接使用成员函数而不必再次声明了;
同时,函数的返回类型通常出现在函数名之前,因此如果成员函数的定义在类的外部时,返回类型就必须指明它是哪个类的成员
例如:

1
2
3
4
5
window_mgr::Screenindex window_mgr::addscreen(const Screen &s)
{
screens.push_back(s);
return screens.size()-1;
}

编译器处理完类中的全部声明后才会处理成员函数的定义;因此函数可以使用类定义中的任何名字。

成员函数查找名字采取以下方式:

首先,在类内部成员函数之前查找该名字的声明;
其次,如果没有找到,则在类内部的其他地方继续查找;
最后,如果整个类都没有这个名字的声明,那么在成员函数定义之前的作用域内进行查找;

如果在成员函数内需要用到外层作用域内的名字,可以显性地使用作用域运算符来体现;

聚合类

当一个类满足下列条件时,它是聚合的:
所有成员都是public的;
没有定义任何构造函数;
没有类内初始值;

聚合类的一大特点是可以用一个花括号括起来的成员初始值列表进行初始化;
例如:

1
2
3
4
5
struct Data{
int ival;string s;
};

Data val={0,"Anna"};

注意:初始值的顺序必须与声明顺序一致

类的静态成员

在类的成员声明之前加上static使得它可以与类关联在一起,称为静态成员。静态成员可以是public或者private的,类型可以是常量,引用,指针,类类型等。
类的静态成员独立于任何对象之外。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<bits/stdc++.h>
using namespace std;
class Account{
public:
void calculate(){amount+=amount*interestrate;}
double rate(){return interestrate;}
static void rate(double);
private:
std::string owner;
double amount;
static constexpr double interestrate=6;
static double initrate();
};
//double Account::interestrate=5;
//void Account::rate(double newrate1)
//{
// interestrate=newrate1;
//}
int main()
{
Account a,b;
//a.rate(6.04);
cout<<a.rate()<<' '<<b.rate()<<endl;

return 0;
}

在上面这个例子中,每个Account对象都拥有自己的两个数据成员owneramount,但是只存在一个interestRate并且被所有Account对象共享,也就是说,当这个值发生变化时,所有对象中的interestrate的值都会变化。

静态成员函数不包含this指针,也不能被声明为const的。

虽然静态成员不属于任何一个对象,但是依然可以使用类的对象、引用和指针来访问静态成员。

对于静态成员函数,也可以在类的外部进行定义,但是不能重复static关键字,也就是说,关键字只能出现在类内部声明的时候。

注意:一般来说,不能在类内部初始化静态成员,必须在类的外部定义和初始化每个静态成员,并且,每个静态数据成员只能定义一次。当然,如果静态成员是字面值常量类型的constexpr,那么我们乐意在类内部提供const整数类型的初始值给静态成员

例如上面那个例子中,如果删去interestRate前面的constexpr,那么就必须在类外部对其进行初始化,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<bits/stdc++.h>
using namespace std;
class Account{
public:
void calculate(){amount+=amount*interestrate;}
double rate(){return interestrate;}
static void rate(double);
private:
std::string owner;
double amount;
static double interestrate;//注意此时就不能在类内部初始化了
static double initrate();
};
double Account::interestrate=5;
void Account::rate(double newrate1)
{
interestrate=newrate1;
}
int main()
{
Account a,b;
//a.rate(6.04);
cout<<a.rate()<<' '<<b.rate()<<endl;

return 0;
}

静态成员还有一个用处就是可以作为成员函数的默认实参。