Fork me on GitHub

类的基本概念

一、前言

介绍一些类的基本概念,包括类的基本思想,函数成员和数据成员,static成员,友元等。

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

封装实现了类的接口和实现的分离。封装后的类隐藏了它的实现细节,也就是说,类的用户只能使用接口而无法访问实现部分。

定义成员函数

尽管所有成员函数都必须在类的内部声明,但是成员函数体可以定义在类内也可以定义在类外。

Sales_data的一个成员函数

1
string isbn() const { return bookNo;}

引入this指针

1
2
Sales_data total;
total.isbn();

在这里,使用点运算符来访问total对象的isbn成员,然后再调用它。

当我们调用成员函数时,实际上是在替某个对象调用它。它隐式地指向调用该函数的对象的成员。实际上隐式地返回 total.bookNo。

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

1
total.isbn()

相当于:

1
2
//伪代码,用于说明调用成员函数的实际执行过程
Sales_data::isbn(&total)

其中调用Sales_data的isbn成员时传入了total地址。

this是一个常量指针,我们不允许改变。

引入const成员函数

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

C++允许把const关键字放在成员函数的参数列表之后,此时,紧跟在参数列表后面的const表示this是一个指向常量的指针(const Sales_data const this)。*像这样使用const的成员函数被称作常量成员函数

因为this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内存。

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

类作用域和成员函数

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

类外部定义的成员的名字必须包含它所属的类名。

定义类相关的非成员函数

一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。在这种方式下,用户使用接口的任何部分都只需要引入一个文件。

点运算符和箭头运算符(. ->)

1
2
3
Person p1 ,*p2;
p1.getname(); //针对于对象来使用的
p2->getname(); //针对于指针来使用的,等价于*p2.getname();

构造函数

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫做构造函数

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

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

知识点1:构造函数—特殊的成员函数,用来控制对象的初始化过程。无返回类型,可重载,不能被声明为const.

知识点2:若无,则有默认的构造函数,是编译器自己隐式的定义的。又称合成的默认构造函数。

知识点3:某些类是不能使用默认的构造函数的,以下三个原因:

1:在未声明任何构造函数的前提下,类内对象的初始化将不受控制

2:合成的默认构造函数可能会造成不必要的错误,如若没有类内初始值来初始化成员,可能这些成员将是未定义的。

3:如果类中包含了一个其他类类型的成员,且这个成员的类型没有默认构造函数,那么编译器将无法初始化该对象。

所以,在撰写类的时候,最好定义一个自己的构造函数。

代码示例

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
#include <iostream>
#include <string>
using namespace std;
class Person; //前向声明Person类
istream& read(istream& is,Person& person); //声明read函数
class Person{
public:
Person() = default; //默认构造函数 等价于Person(){};
Person(string name,string address); //重载构造函数
Person(istream &is){ //重载构造函数,通过调用非成员函数read对其进行赋值
read(is,*this);
}
void show();
const string& getname(){
return this->name;
}
const string& getaddress(){
return this->address;
}
void setname(string name){
this->name = name;
}
void setaddress(string address){
this->address = address;
}
Person& combine(const Person &person);
private:
string name;
string address;
};
void Person::show(){
cout<<"name:"<<this->name<<" address:"<<this->address<<endl;
}
Person::Person(string name,string address):name(name),address(address){
}
Person& Person::combine(const Person &person){
name += person.name;
return *this;
}
istream& read(istream& is,Person& person){ //非成员函数
string name ,address;
is>>name>>address;
person.setname(name);
person.setaddress(address);
return is;
}
int main()
{
Person p1("wangxinri","cqu");
p1.show();
Person p2("ri","cqu");
p1.combine(p2);
p1.show();
cout<<p2.getname()<<endl;
cout<<p2.getaddress()<<endl;
Person p3(cin);
p3.show();
return 0;
}

拷贝、赋值和析构

如果我们不主动定义这些操作,则编译器将替我们合成它们。一般来说,编译器生成的版本将对对象的每个成员执行拷贝、赋值和销毁操作。

对于某些类来说合成的版本无法正常工作,管理动态内存的类通常不能依赖于上述操作的合成版本。

使用vector或者string的类能避免分配和释放内存带来的复杂性。如果类包含vector或者string成员,则其拷贝、赋值和销毁的合成版本能够正常工作。

类的静态成员

有的时候类需要它的一些成员与类本身直接相关,而不是与类的各个对象保持关联。例如,一个银行账号类可能需要一个数据成员来表示当前的基准利率。在此例中,我们希望利率与类关联,而非与类的每个对象关联。从实现效率的角度来看,没必要每个对象都存储利率信息。而且更加重要的是,一旦利率浮动,我们希望所有的对象都能使用新值。

声明静态成员

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

举个例子,我们定义一个类,用它表示银行的账户记录:

1
2
3
4
5
6
7
8
9
10
11
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();
};

类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。 因此,每个Account对象将包含两个数据成员:owner和amount。只存在一个interestRate对象而且它被所有Account对象共享。

类似的,静态成员函数也不与任何对象绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的(const是用来修饰this指针类型的),而且我们也不能在static函数体内使用this指针,这一限制既适用于this的显式使用,也对调用非静态成员的隐式使用有效。静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。静态成员函数不可以同时声明为 virtual、const、volatile函数。

1
2
3
4
5
class base{
virtual static void func1();//错误
static void func2() const;//错误
static void func3() volatile;//错误
};

使用类的静态成员

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

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

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

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

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

1
2
3
4
5
6
class Account{
public:
  void calculate() {amount+=amount*interestRate;}
private:
  static double interestRate;
};

定义静态成员

和其他的成员函数一样,我们既可以在类的内部也可以在类的外部定义静态成员函数。当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部的声明语句

1
2
3
4
void Account::rate(double newRate)
{
  interestRate=newRate;
}

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

因为静态数据成员不属于类的任何一个对象,所以它们并不是在创建类的对象时被定义的。这意味着它们不是由类的构造函数初始化的。而且一般来说,我们不能在类的内部初始化静态数据成员。相反的,必须在类的外部定义和初始化每个静态成员。和其他对象一样,一个静态数据成员只能定义一次。

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

我们定义静态数据成员的方式和在类的外部定义成员函数差不多。我们需要指定对象的类型名,然后是类名、作用域运算符以及成员自己的名字:

1
2
//定义并初始化一个静态成员
double Account::interestRate=initRate();

这条语句定义了名为interestRate的对象,该对象是类Account的静态成员,其类型是double。从类名开始,这条定义语句的剩余部分就都位于类的作用域之内了。因此,我们可以直接使用initRate函数。注意,虽然initRate是私有的,我们也能使用它初始化interestRate。和其他成员的定义一样,interestRate的定义也可以访问类的私有成员。

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

静态成员的类内初始化(一般来讲,我们在类的外部定义和初始化)

通常情况下,类的静态成员不应该在类的内部初始化。然而,我们可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr。初始值必须是常量表达式,因为这些成员本身就是常量表达式,所以它们能用在所有适用于常量表达式的地方。例如,我们可以用一个初始化了的静态数据成员执行数组成员的维度:

1
2
3
4
5
6
7
8
class Account{
public:
static double rate() { return interestRate;}
static void rate(double);
private:
static constexpr int period=30;
double daily_tbl[period]; //period是常量表达式
};

如果在类的内部提供了一个初始值,则成员的定义不能再指定一个初始值了

1
2
//一个不带初始值的静态成员的定义
constexpr int Account::period; //初始值在类的定义内提供
即使一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员

静态成员能用于某些场景,而普遍成员不能

如我们所见,静态成员独立于任何对象。因为,在某些非静态数据成员可能非法的场合,静态 成员却可以正常地使用。举个例子,静态数据成员可以是不完全类型。特别地,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则受到限制,只能声明成它所属类的指针或引用。

1
2
3
4
5
6
7
8
class Bar{
public:
//......
private:
static Bar mem1; //正确:静态成员可以是不完全类型
Bar *mem2; //正确:指针成员可以使不完全类型
Bar mem3; //错误:数据成员必须是完全类型
};

静态成员和普通成员的另一个区别是我们可以使用静态成员作为默认实参

1
2
3
4
5
6
7
class Screen{
public:
//bkground 不是一个在类中稍后定义的静态成员
Screen &clear(char =bkground);
private:
static const char bkground;
};

非静态数据成员不能作为默认实参,因为它的值本身属于对象的一部分,这么做的结果是无法真正提供一个对象以从中获取成员的值,最终引发错误。

简单代码示例

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
#include <iostream>
#include <string>
using namespace std;
class Account{
public:
static double rate() { return interestRate; }
static void rate(double Rate);
private:
string owner;
static double interestRate ;
static const int aa = 0; //正确
static constexpr int bb = 0; //正确,constexpr类型可以类内初始化
//static double cc = 0; //错误
};
//外层定义static成员不需要加上static
double Account::interestRate = 1.0; //定义并初始化一个成员
void Account::rate(double Rate){
interestRate = Rate;
}
int main()
{
Account a1,a2;
cout<<a1.rate()<<" "<<a2.rate()<<endl;
a1.rate(2.0);
cout<<a1.rate()<<" "<<a2.rate()<<endl;
return 0;
}

友元(friend)

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

友元函数只能出现在类定义的内部,但是在类内出现的位置不限。友元不是类的成员函数也不受它所在区域访问控制级别的约束。

一般来说,最好在类定义开始或者结束前的位置集中声明友元。

友元的声明

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明,如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明。

友元再探

类还可以把其他的类定义成友元,也可以把其他类(之前已定义过的)的成员函数定义成友元。此外,友元函数能定义在类的内部,这样的函数时隐式内联的。

类之间的友元关系

1
2
3
4
5
class Screen{
//Window_megr的成员可以访问Screen类的私有部分。
friend class Window_megr; 把Window_megr指定成Screen的友元
//Screen类的剩余部分
}

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

必须注意的是,友元关系不存在传递性。也就是说,如果Window_mgr有它自己的友元,则这些友元并不能理所当然地具有访问Screen的特权。

令成员函数作为友元

除了令整个Window_mgr作为友元之外,Screen还可以只为Window_mgr成员函数clear提供访问权限。当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类:

1
2
3
4
5
class Screen{
//Window_mgr::clear必须在Screen类之前被声明
friend void Window_mgr::clear(ScreenIndex);
//Screen类的剩余部分
};

要想令某个成员函数作为友元,我们必须仔细组织程序的结构以满足声明和定义的彼此依赖关系。在这个例子中,我们必须按照如下方式设计程序:

  • 首先定义Window_mgr类,其中声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen。
  • 接下来定义Screen,包括对于clear的友元声明
  • 最后定义clear,此时它才可以使用Screen的成员。

函数重载和友元

尽管重载函数的名字相同,但他们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明:

1
2
3
4
5
6
7
//重载的storeOn函数
extern ostream& storeOn(ostream& ,Screen &);
extern BitMap &storeOn(BitMap& ,Screen &);
class Screen{
//StoreOnde ostream版本能访问Screen对象的私有部分
friend ostream::ostream& storeOn(ostream& ,Screen);
};

Screen类把接受ostream&的storeOn函数声明成它的友元,但是接受BitMap&作为参数的版本仍然不能访问Screen。

友元声明和作用域

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

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

1
2
3
4
5
6
7
8
9
struct X{
friend void f() { /*友元函数可以定义在类的内部*/}
X() {f();} //错误:f还没有被声明
void g();
void h();
};
void X::g(){return f();} //错误:f还没有被声明
void f(); //声明那个定义在X中的函数
void X::h() {return f();} //正确:现在f的声明在作用域中了

关于这段代码最重要的是理解友元声明的作用是影响访问权限,它本身并非普通意义上的声明。

简单友元类示例

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
#include <iostream>
using namespace std;
class student{
friend class teacher;
public:
void show_info(){
cout<<"name:"<<name<<" address"<<endl;
}
private:
string name = "xin";
string address = "cqu";
};
class teacher{
public:
void show_info(){
cout<<"name:"<<name<<" address"<<endl;
}
void showStudent(student &STD);
private:
string name;
string address;
};
void teacher::showStudent(student &STD){
cout<<STD.name<<" "<<STD.address<<endl;
}
int main()
{
student STD;
STD.show_info();
teacher TEA;
TEA.showStudent(STD);
return 0;
}
-------------本文结束感谢您的阅读-------------

本文地址:http://www.wangxinri.cn/2017/10/22/类的基本概念/
转载请注明出处,谢谢!

梦想夹带眼泪,咸咸的汗水!