《C++ Primer》学习笔记(第七章)——类

<本章复习完,书上第一部分的内容也就复习完了,接下来打算先跳过第二部分,直接复习第三部分,这样可以把类的相关内容放在一起复习,然后回过头在复习第二部分吧!>

/1、类可以使用关键字class也可以使用struct,两者唯一的区别是默认访问权限不一样。class的默认访问权限是private,struct的默认访问权限是public。类的访问权限有public/private/protected。
①、public成员能够在整个程序内被访问,public成员定义了类的接口;
②、private成员只能被类的成员访问,类的对象也不能访问private成员;
③、protected在后续讲到类的继承时在记录吧。
访问说明符的有效范围为从声明开始,到出现下一个访问说明符或到达类的结尾为止

2、对于两个类来讲,即使他们的成员完全一样,这两个类也是不同的类型。

3、如同函数的声明和定义可以分离开,我们也可以把类的定义和声明分离开,我们可以先对类进行声明,但是不定义它,如:

class A;//只声明,不定义

类似于A的声明称为前向声明,在A的声明之后定义之间A属于不完全类型对于不完全类型,我们可以定义指向该类型的指针或者引用,也可以声明以不完全类型为参数和返回类型的函数,但是不能创建不完全类型的对象,因为类的对象创建之前该类必须被定义才行。但是静态成员可以是不完全类型。如:

class A{
public:
A a1;//错误
A *p;//正确
static A a2;//正确

1、类的成员函数

类的成员函数只能在类内声明,但是可以在类内或者类外定义。在类内定义的成员函数会隐式的成为inline函数。

当某个类的不同对象调用类的成员函数时,成员函数如何区分不同的类对象呢?实际上成员函数是通过名为一个this的隐式参数来访问调用它的那个对象,当某个对象调用成员函数时,用该对象的地址初始化this,从而达到成员函数能够识别不同对象的目的,如下伪代码所示,是伪代码,伪代码,伪代码:

class A{
public :
     int func(){return num};
private:
     int num;
}
class a1,a2;
a1.func();  //隐式参数this=&a1,返回的是this->num;
a2.func();  //隐式参数this=&a2,返回的是this->num;

this是隐式定义的,不能自定义名为this的参数或者变量this是一个常量指针,因此不能改变this中保存的地址。另外可以在成员函数体内部已经隐式的地使用了this ,但是还是可以显示的使用this,如上述的func函数:

int func(){return this->num};  //显示使用this

默认情况下,this是指向非常量类对象的常量指针,因此不能将this绑定到常量类对象上,因此对于常量类对象只能调用常量成员函数。const成员函数的声明如下:

int func() const {return num};//常量成员函数

常量成员函数的this指针实际上是指向常量的常量指针,即const A* const this。常量成员函数不能改变调用它的对象的内容,即常量成员函数体中可以含有非常量数据成员,但是不能修改这些数据成员的值。如果真想在常量成员函数中修改某一个数据成员,那么可以把该数据成员声明为可变数据成员,即用mutable,另外我们不能把const类型的数据成员声明为可变数据成员

常量对象以及常量对象的指针或引用只能调用常量成员函数,非常量对象既可以调用普通的成员函数,也可以调用常量成员函数。好比常量对象只能绑定到指向常量的指针,而非常量对象既可以绑定到指向非常量的指针,也可以绑定到指向常量的指针。另外常量成员函数和普通成员函数可以构成函数重载。
只要理解常量成员函数的const是一个底层const,且是用来修饰this指针的,上述内容就好理解了)

常量成员函数无论是声明还是定义时,都需要加上const关键字,哪怕在类外定义常量成员函数也需要在形参列表后加上const。由于编译器先编译成员的声明,然后在轮到函数体,因此函数体中可以任意使用其他成员而无需在意这些成员出现的次序。

2、返回*this的成员函数
某个成员函数返回类型是该类的引用时,此时函数将会返回调用该函数的对象的引用不会发生拷贝。如在上面的类A中再定义如下成员函数:

A &AddNum(int i){
num=num+i;
return *this;  //返回调用该函数的对象引用
}
//
A a1;
a1.AddNum(10);  //返回a1,且为左值。

如果常量成员函数返回*this,那么不管调用常量成员函数的是常量还是非常量对象,返回的都是一个常量引用。如:

const A &ShowNum() const {  //常量成员函数以引用的形式返回*this,则返回类型为常量引用
cout<<num<<endl;
return *this;  //返回该对象的常量引用
};
A a2;
a2.ShowNum().AddNum(10);  //错误返回a2的常量引用,即a2.ShowNum()为a2的常量引用,不能调用非常量成员函数AddNum();  但是a2本身是一个非常量,因此可以调用非常量成员函数。

如上述代码所示,一定要把a2.ShowNum()和a2两者区分开,a2.ShowNum()为对a2的常量引用,因此只能调用常量成员函数,而a2本身仍然是一个非常量因此可以调用常量成员函数,也可以调用非常量成员函数

3、构造函数

构造函数的任务就是初始化类对象的数据成员,无论何时只要类的对象被创建,就会执行构造函数。构造函数的没有返回类型,名字与类名一样。构造函数不能声明成const,当创建一个const类对象时,直到构造函数完成初始化过程,对象才能真正取得常量属性。
①、如果一个类没有声明任何构造函数,那么系统会提供一个隐式的默认构造函数,默认构造函数没有任何参数。当我们声明了构造函数后,系统将不会提供默认构造函数,如果仍然需要默认构造函数,可以使用default要求编译器生成默认构造函数,如:

class A{
public:
A()=default;  //默认构造函数
A(int i):num(i){};//自定义构造函数
private:
int num;
}

②、某些情况下我们必须自己定义构造函数,比如类A中的一个成员是类B的对象,而类B没有默认构造函数,此时编译器提供的默认构造函数将不能初始化类B的对象,这种情况就需要自己定义对类A的构造函数。

③、构造函数不用为类的所有数据成员赋值,没有出现在构造函数初始化列表中的数据成员将执行默认初始化。由于构造函数的唯一目的就是为数据成员赋值,因此函数体为空。

④、如果没有在构造函数的初始化列表中显示的初始化成员,而是在构造函数体内初始化成员,那么实际上数据成员是在构造函数体之前先执行默认初始化,然后在函数体中被赋值

class A{
public:
  A(int i,int j) //由于没有初始化列表,实际上此时a和b先执行默认初始化,然后在函数体中被赋值的
  {
  a=i;
  b=j;
  }
private:
int a;
int b;
}

⑤、对于const成员,引用或者某种没有默认构造函数的类类型,我们只能在构造函数的初始化列表中对其进行初始化,而不能在函数体中对其进行赋值。

⑥、成员初始化的顺序与类定义中出现的顺序一致,而与初始化列表中出现的顺序无关。

class A{
int i;
int j;
public:
 X(int num):j(num),i(j){};//先初始化i,由于此时j还为定义,因此此时i是一个不确定的值
 }

⑦、我们也可以为构造函数提供默认实参,含有默认实参的构造函数也可以提供类的默认构造函数

class A{
int i;
public:
 X(int num=10):i(num){};//含有默认实参的构造函数,创建A的对象时可以不提供参数,达到默认构造函数的目的。
 }

委托构造函数
简单讲3就是先定义一个基本的构造函数,然后其他构造函数委托这个基本构造函数来进行初始化功能,这就是委托构造函数,好比抱大腿,如:

A(string s,int i,double d):Name(s),Age(i),Heigh(d){};//一般构造函数
A(): A(" ",0 , 0){};//没有参数的委托构造函数;
A(string s): A(s,0,0){};//含有一个参数的委托构造函数。

委托构造函数的执行顺序为:受委托函数的初始化列表和函数体先被执行,然后在执行委托函数的函数体。

4、友元
①、如果一个类或者函数需要访问某一个类的非共有成员,可以把类或者函数声明为该类的友元,即在函数声明时加上firend关键字。友元函数的声明只能出现在类的内部 。

class A{
friend int func();//函数func声明为类A的友元,此时函数func可以访问类所有成员。

②、还可以为一个类指定友元类,那么友元类的成员函数都可以访问此类的所有成员。另外友元关系不存在传递性,比如类A的友元类是类B,类B的友元类是类C,那么类C不能访问类A中的私有成员的。

class B{//};  //类B
class A(
friend class B;  //声明类B为类A的友元类,类B中的成员可以访问类A中的所有成员。
//
};

③、除了将整个类声明为友元类以外,还可以将类中的某些成员函数声明为友元,不过此时需要明确指出,该成员函数属于哪个类:

class A(
friend int B::func();  //声明类B中的成员函数func为类A的友元。
//
};

值得注意的是:我们需要先声明类B,以及声明函数func但是不能定义它,因为func是类A的友元,func需要使用类A中的数据成员,因此应该先定义类A并对func进行友元声明,然后才是定义func。

④、友元可以在类中定义。友元在类中的声明仅仅是指定了访问权限,并非真正的声明,如果希望类的用户能够调用某个友元函数,我们需要在类外再对友元进行声明,这样才能使其可见

class A{
 public:
   friend void func(){  //  };//友元函数可以定义在类内部,并且是隐式inline
   void X(){ func();};//错误,func还未声明
   void Y();
   void Z();
   };
   void A::Y(){ func();};//错误func还未声明
   void func(); //声明func,非成员函数的声明可以在它的友元声明之后。
   void A::Z(){ func();};//正确,func已经声明

5、隐式类型转换
如果一个类的构造函数只接受一个实参,那么实际上定义了一种转换成该类类型的隐式转换机制,这种构造函数称作为转换构造函数。注意:只有一个实参的构造函数才能 实现隐式转换机制,含有多个实参的构造函数不能实现隐式转换机制。如:

class A;
string name;
public :
 A(string s):name(s){}; //只含一个参数的构造函数,可以执行隐式转换
 };
 A a1("IG");//直接初始化
 string name="RNG";
 A a2=name;//隐式类型转换

如上述代码所示:尽管a2和name属于不同的类型,但是我们可以令a2=name,实际上执行了隐式类型转换过程。该过程实际上分两步:
①、编译器首先调用构造函数生成一个临时的A类对象:A temp(name);
②、然后在用这个临时的对象对a2进行拷贝:a2=temp。

注意:编译器只能自动执行一步类型转换,如果上述代码改为:

A a3="EDG";  //错误,执行了两次类型转换,而编译器只能自动执行一次类型转换

上述代码中:编译器首先将字面值类型“EDG”转换为string类型,然后在将string类型转换成A类型,因此实际上是两步类型转换,而编译器只能自动执行一步类型转换,所以需要手动将字面值类型转换为string类型,如:

string s="EDG";
A a3=s;//正确

如果想阻止这种构造函数的隐式类型转换,可以在构造函数前加上关键字explicit,那么此时构造函数只能用于直接初始化,而不能进行拷贝初始化(使用=)。如上述构造函数改为:

explicit A(string s):name(s){};//只能进行直接初始化
//
A a1("IG");//正确,直接初始化
string s="RNG";
A a2=s;//错误,不能进行印刷类型转换,不能用于拷贝初始化

当然我们也可以使explicit构造函数显示地进行强制类型转换,可以使用static_cast,如:

string s="EDG";
A a3=static_cast<A>(s);//强制类型转换

6、类的静态成员
1、在类的成员声明之前加上关键字static可以使该成员与类关联在一起。静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。静态成员函数由于不与任何对象绑定在一起,因此也不存在this指针,也就不能将静态成员函数声明为const,因为const成员函数的const本来就是修饰this指针的,既然都没有this 指针也就不能有const了。

2、静态成员函数:静态成员函数只能访问类内的静态数据成员,可以在类内部也可以在类外部定义静态成员函数,不过在类外部定义静态成员函数时,不能重复static关键字,该关键字只能出现在类内部的声名语句中。我们可以通过作用域运算符访问静态成员函数,也可以通过类对象,引用或者指针来访问静态成员函数,因为静态成员函数为所有对象共有。

3、静态数据成员:静态数据成员一旦被定义,将会存在与程序的整个生命周期,另外对于非常量的静态成员,是可以通过作用域运算符,或者类对象来修改该成员的值的。非常量静态数据成员一般只能在类内声明,在类外定义,不能在类内部定义静态数据成员,因为静态成员属于类不属于对象,对所有对象来说静态成员只有一份,如果定义来类的内部,那么会随着类对象的初始化而不断初始化。如果静态数据成员是常量或者constexpr,那么可以在类内定义。与静态成员函数类似,在类外定义静态数据成员时,不能重复static,另外静态成员可以是一个不完全类型(之前也提到过),静态成员也可以作为默认实参,而非静态类型则不能。举个栗子:

class A {
private:
	string name;
public:
	explicit A(string s) :name(s) {};  //explicit阻止隐式类型转换
	string GetName() { return name; };
	static void shownum() { cout << num << endl; };  //静态成员函数可以在类内定义,且只能访问静态成员
    static int num;  //静态数据成员不能在类内被定义
};
  int A::num = 10;

int main()
{
	A a1("IG");  //直接初始化
	string s = "RNG";
	A a2 = static_cast<A>(s);  //强制类型转换
	a1.shownum();  //可以通过类对象调用静态成员函数
	A::num = 20;  //可以通过作用域运算符修改非常量静态成员
	A::shownum();  //通过作用域运算符调用静态成员函数
	a1.num = 30;  //通过对象修改非常量静态数据成员
    return 0;
}

第七章看完,本来是想按书上的顺序往下看的,但是书的第二部分的内容是《c++标准库》,而类的其他内容却出现在后面。为了把类的相关内容放在一起,打算先复习第三部分《类设计者的工具》然后在回过头复习第二部分吧。就这样,吃饭去先!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值