一、类
(一)访问控制属性
如果私有成员紧接着类名称,则关键字private可以省略
在类中,未指定访问控制属性的成员,其访问控制属性默认为私有类型
在结构体和联合体中,未指定访问控制属性的成员,其访问控制属性默认为公有类型
在类的成员函数中,既可以访问目的对象的私有成员,又可以访问当前类的其他对象的私有成员。例:
class dog
{
private:
string name;
public:
dog()
{name="none";}
dog(string s)//改用dog(string s="none"),可省去上两行的重载
{name=s;}
void show(dog b)
{cout<<name<<" "<<b.name<<endl;}//b.name为当前类其他对象的私有成员
};
int main()
{
dog a;
dog b("123");
a.show(b);
}
(二)类成员函数(默认形参、内联)
有默认值的形参必须在形参列表的最后,也就是说,有默认值的形参右面不能出现无默认值的形参。
类成员函数可有默认形参值,默认值必须写在类定义中,而不能写在类定义之外的函数实现中(但实测可以,但只能写在一处,两处相同也不可)
class dog
{
private:
string name;
public:
dog(string s="none"){name=s;}
void bark(string s);
};
void dog::bark(string s="wang"){cout<<s;}
int main()
{
dog a;
a.bark(); //输出wang
}
内联函数隐式声明,即为上述代码所示
显示声明,类外实现函数时加inline
inline void dog::show(dog b)
{cout<<name<<" "<<b.name<<endl;}
(三)构造、复制、析构
1.构造函数
无需提供参数的构造函数称为默认构造函数
如果类中没有写构造函数,编译器会自动生成一个隐含的默认构造函数(参数列表和函数体皆为空)。如果声明了构造函数(无论是否有参数),编译器便不会再为之生成隐含的构造函数
2.复制构造函数
声明: 类名(类名 & 对象名);
实现: 类名::类名(类名 & 对象名);
用类的一个对象为另一个对象赋值,不会调用复制“构造”函数,如
Point a(1,2);
Point b; // 对象b已经存在,无需“构造”
b=a; // 使用赋值运算符来赋值即可
当用返回的对象b(函数中的局部变量)为一个对象a赋值时,若数据成员含有指针类型,则a中该指针与b中指向相同。所以随着所调用函数中局部对象b的消亡,a中该指针成员将变为野指针。例见Polynomial类
复制构造函数被调用的3种情况:
- 用类的一个对象去初始化该类另一个对象
Point a(1,2);
Point b(a);
Point c=a;
- 形参是类的对象,调用函数时,进行形参和实参结合
但只有对象用值传递时,才会调用复制构造函数,如果传递引用则不会。所以传递较大对象时,传递引用会比传值的效率高很多 - 函数返回值是类的对象,函数执行完成返回调用者时
因为a是局部对象,在return a之后还为来及赋值给b,a就已消亡。所以在b=f()的过程中创建了临时对象,先用a初始化临时对象,再用临时对象为b赋值,计算完b=f() 临时对象便消亡
Point f()
{ Point a(1,2); return a;}
int main()
{ Point b;b=f();}
自定义复制构造函数相比隐含的复制构造函数更灵活。且当类的数据成员中有指针类型时,默认的复制构造函数实现的只能是浅复制,会带来数据安全方面隐患。
3.析构函数
~Clock(){} //空的内联析构函数,其功能和系统自动生成的隐含析构函数相同
二、类的组合
(一)组合类的构造函数
类名::类名(形参表):内嵌对象1(形参表),内嵌对象2(形参表),…
“:”后称做初始化列表
- 基本类型的数据成员也可这样初始化
- 内嵌对象构造函数的调用顺序按照内嵌对象在组合类的定义中出现的顺序,与在初始化列表中出现的顺序无关
- 组合类的默认构造函数被调用时,内嵌对象的默认形式构造函数也会被调。这时隐含的默认构造函数并非什么也不做
- 没有默认构造函数的内嵌对象和引用类型的数据成员(必须在初始化时绑定引用的对象)的初始化必须在初始化列表中进行
如果一个类包括这两类成员,那么编译器不能为这个类提供隐含的默认构造函数。必须编写显示的构造函数,并且在每个构造函数的初始化列表中至少为这两类数据成员初始化。
(二)组合类的复制构造函数
Line::Line(Line &l):p1(l.p1) ,p2(l.p2) {…} //Line类包含Point类的对象p作为成员
Point p1(1,1),p2(4,5);
Line l1(p1,p2);
//参数形实结合时2次+初始化内嵌对象2次=4次point复制构造
// 1次line构造
Line l2(l1);
//初始化内嵌对象,2次point复制构造
// 1次line复制构造
(三)前向引用声明
定义A类时,要使用尚未定义的B类的对象,须在定义A类前添加
class B;//前向引用声明
class A{ .... }; //其中包含B类对象
尽管使用了前向引用声明,但在提供一个完整的类定义之前,不能定义该类的对象,也不能在内联成员函数中使用该类的对象。
但可以声明该类的对象引用或指针
即只能使用被声明的符号,而不能涉及类的任何细节
三、数据共享与保护
(一)静态
static 加在最前面
定义时未指定初值的基本类型静态生存期变量,会被赋予0值初始化,而对于动态生存期变量,不指定初值意味着初值不确定
类中的数据成员可以被同一类中的任何一个函数访问
在类的定义中仅仅对静态数据成员进行引用性声明,必须在命名空间作用域的某个地方使用类名限定定义性声明,这时也可以进行初始化
class Point{...static int count;...};
int Point::count=0; //无需写static
静态成员函数可以直接访问该类的静态(数据和函数)成员。而访问非静态成员,必须通过对象名。
非成员函数不能有CV(const和volatile)限定,静态成员函数不能有CV限定
(二)类的友元
在类定义中声明友元函数或友元类(前加friend),友元函数的定义在类外(无需加friend)
- 友元关系是不能传递的,A信任B,B信任C,但A未必信任C
- 友元关系是单向的,A信任B但B未必信任A
- 友元关系是不被继承的,A信任B但未必信任B的孩子
(三)const
常对象必须初始化,且不能被更新。
const关键字也可放在类型名后
- 常对象的数据成员都被视同为常量。
- 不能通过常对象调用普通的成员函数,常对象只能调用它的常成员函数
- 无论是否通过常对象调用常成员函数,在常成员函数调用期间,目的对象都被视为常对象。因此常成员函数不能更新目的对象的数据成员,也不能针对目的对象调用该类中没有用const修饰的成员函数(所以常成员函数中不可调用其数据成员(某类对象)的非const成员函数)
- const可用于类中函数重载,加在声明语句最后(void print()const;)
- const是函数类型的一个组成部分,因此在函数的定义部分也要带const
- 静态成员函数和非成员函数不能有CV(const和volatile)限定
void R::print()const
{...}
构造函数对常数据成员进行初始化,只能通过初始化列表
class A
{
public:
A(int i);
private:
const int a; //常数据成员
static const int b; //静态常数据成员
};
const int A::b=10; //静态常数据成员在类外说明和初始化
A::A(int i):a(i){} //常数据成员只能通过初始化列表来获得初值
例外:类的静态常量如果具有整数类型或枚举类型,可以直接在类定义中为它指定常量值,如上例可直接在类定义中写:static const int b=10;
非const的引用只能绑定到普通对象,而不能绑定到常对象,但常引用可以绑定到常对象。一个常引用无论是绑定到普通对象还是常对象,通过该引用访问该对象时,都只能把该对象当作常对象
形参为常引用,实参可非const,因为常引用可以绑定到普通对象。
(四)外部变量
- 命名空间作用域中定义的变量,默认情况下都是外部变量,在其他文件中若需使用这一变量,需要用extern关键字加以声明。
- 在所有类之外声明的函数,都是具有命名空间作用域的,如果没有特殊说明,这样的函数都可以再不同的编译单元中被调用,只要在调用之前进行引用性声明即可。
- 命名空间作用域中声明的变量和函数,在默认情况下都可以被其他编译单元访问。若不希望如此,则可以在定义这些变量和函数时使用static关键字(与extern关键字起相反作用)。另外可以使用匿名的命名空间,在匿名命名空间中定义的变量和函数不会暴露给其他编译单元。
namespace //匿名的命名空间
{
int n;
void f(){n++};
}
四、数组、指针与字符串
(一)指向常量的指针
不能通过指针来改变所指对象的值,但指针本身可以改变
int a;
const int *p1=&a; //指向常量的指针
(二)指针类型的常量
指针本身的值不能被改变
int * const p2=&a; //指针类型的常量
(三)指针型函数
返回值为指针类型,一般定义形式如下,
数据类型 *函数名(参数表){ }
(四)指向函数的指针
一般语法如下,
数据类型 (*函数指针名)(形参表)
(五)指向类的非静态成员的指针
//声明语句的一般形式:
类型说明符 类名::*指针名;
类型说明符 (类名::*指针名)(参数表);
//赋值语句一般形式:
指针名=&类名::数据成员名;
指针名=&类名::成员函数名;
//访问形式:
对象名.*类成员指针名或对象指针名->*类成员指针名
(对象名.*类成员指针名)(参数表)或(对象指针名->*类成员指针名)(参数表)
类的定义只确定了各个数据成员的类型、所占内存大小以及它们的相对位置,在定义时并不为数据成员分配具体地址。所以经上述赋值之后,只是说明了被赋值的指针是专门用于指向哪个数据成员的,同时在指针中存放该数据成员在类中的相对位置,通过这样的指针现在并不能访问什么
能够被常成员函数赋值的指针需要在声明时明确写出const关键字,如:
int (Point::*funcPtr)() const=&Point::getX;//getX为常成员函数
(六)指向类的静态成员的指针
对类的静态成员的访问不依赖于对象,因此可以用普通的指针来指向和访问静态成员
(七)深浅复制
浅复制,可能使对象a、b指向同一内存地址,而并未正常产生a的副本,当a的析构函数调用后,该共同的内存空间被释放,b的析构函数调用时,同一空间被再次释放,导致运行错误
五、继承与派生
如果不显式地给出继承方式关键字,系统的默认值就认为是私有继承
继承除构造和析构函数之外的所有非静态成员
(一)访问控制
公有继承:基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问
私有继承:基类的公有成员和保护成员都以私有成员身份出现在派生类中,而基类的私有成员不可直接访问
保护继承:基类的公有成员和保护成员都以保护成员身份出现在派生类中,而基类的私有成员不可直接访问
(二)类型兼容规则
需要基类对象的任何地方,都可以使用公有派生类的对象来替代
- 派生类的对象可以隐含转换为基类对象(用派生类对象为基类对象赋值)
- 派生类的对象可以初始化基类的引用
- 派生类的指针可以隐含转换为基类的指针
替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员,即仅仅发挥出基类的作用,见P263 例7-3 (结果都是调用基类的成员函数,可用虚函数来实现多态性)
(三)派生类构造函数
如果对基类初始化时,需要调用基类的带有形参表的构造函数时,派生类就必须声明构造函数。
派生类没有显式的构造函数时,系统会隐含生成一个默认构造函数,该函数会使用基类的默认构造函数对继承自基类的数据初始化,并且调用类类型的成员对象的默认构造函数,对这些成员对象初始化。
执行次序:(与初始化列表中顺序无关)
- 调用基类构造函数,顺序按照被继承时声明的顺序
- 对新增成员对象初始化,顺序按照在类中声明顺序
- 执行函数体
派生类的析构函数 执行次序与构造函数完全相反
(四)派生类成员的标识与访问
如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏,解决方法见下。
(只有在相同的作用域中定义的函数才可以重载 )
两个基类含有同名成员var和fun(),且派生类没有声明与基类同名的成员,则“对象名.成员名”无法访问到任何成员。
可使用using关键字加以澄清,使派生类对象可通过“对象名.成员名”访问从指定基类中继承的成员。另外,将using用于基类中的函数名,这样派生类中定义同名但参数不同的函数,基类函数不会被隐藏,两个重载的函数将会并存在派生类的作用域中。
class Derived:public Base1,public Base2
{
public:
using Base1::var;
using Base1::fun;
void fun(int){...}
}
派生类的部分或全部直接基类是从另一个共同的基类派生而来,则该派生类中会产生同名,须用直接基类来进行限定。
这些同名数据在内存中同时拥有多个副本,同一个函数名会有多个映射(函数成员始终只有一个副本 ),很多情况下只需要一个这样的数据副本,多份副本增加了内存开销,可使用虚基类技术解决这问题。
(五)虚基类
class Base0
{
public:
int var0;
void fun0(){...}
Base0(int var):var0(var){}
}
class Base1:virtual public Base0
{
public:
Base1(int var):Base0(var){}
}
class Base2:virtual public Base0
{
public:
Base2(int var):Base0(var){}
}
class Derived:public Base1,public Base2
{
public:
Derived(int var):Base0(var),Base1(var),Base2(var){}
}
Derived d(1);
d.var0=2; //可直接访问虚基类的数据成员
d.fun0(); //可直接访问虚基类的函数成员
如果虚基类声明有非默认形式的(即带形参的)构造函数,并且没有声明默认形式的构造函数,整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中列出对虚基类的初始化。
虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用都自动被忽略
六、多态性
(一)虚函数
1.声明
虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候
class Base
{...virtual void display()const;...}
void Base::display()const
{...}
虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数处理。但将虚函数声明为内联函数也不会引起错误。
2.基类构造函数调用虚函数
当基类构造函数调用虚函数时,不会调用派生类的虚函数。假设有基类Base和派生类Derived,两个类中有虚成员函数virt(),在执行Derived的构造函数时,需先调用Base的构造函数。如果Base构造函数调用了虚函数virt(),则被调用的是Base::virt(),而不是Derived::virt()。因为基类被构造时,对象还不是一个派生类的对象。同样,当基类被析构时,对象不再是一个派生类对象,也不会调用派生类的虚函数。
3.虚函数默认形参值不可变
重写继承来的虚函数时,如果有默认形参值,不要重新定义不同的值。因为虽然虚函数时动态绑定的,但默认形参值是静态绑定的。也就是说,通过一个指向派生类对象的基类指针,可以访问到派生类的虚函数,但默认形参值却只能来自基类的定义。
4.动态绑定
只有通过基类的指针或引用调用虚函数时,才会发生动态绑定。基类的指针可以指向派生类的对象,基类的引用可以作为派生类对象的别名,但基类的对象却不能表示派生类的对象。
5.虚析构函数
Base *b=new Derived(); //Base的析构函数非虚函数
delete b; //仅调用基类的析构函数,内存泄漏
通过基类指针删除派生类对象时,调用的是基类的析构函数,派生类的析构函数没有被执行,因此派生类对象中动态分配的内存空间没有得到释放,造成内存泄漏。
所以这时需让基类的析构函数成为虚函数。
(二)纯虚函数
virtual 函数类型 函数名(参数表)=0;//const加在=0之前,参数表之后
声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。
基类中仍允许对纯虚函数给出实现,但即使给出实现,也必须由派生类覆盖,否则无法实例化。在基类中对纯虚函数定义的函数体的调用,必须通过“基类名::函数名(参数表)”的形式。若将析构函数声明为虚函数,则必须给出实现,因为派生类的析构函数体执行完后需要调用基类的纯虚函数。
(三)抽象类
带有纯虚函数的类是抽象类
抽象类的派生类必须给出所有纯虚函数的函数实现才能实例化
抽象类不能实例化,但可以定义一个抽象类的指针和引用。通过指针或引用,就可以指向并访问派生类的对象,进而访问派生类的成员,这种访问是具有多态特征的
七、模版
(一)函数模板
template<typename/class T>
T abs(T x)
{
return x<0?-x:x;
}
//可像一般函数一样调用
cout<<abs(4.5)<<ends<<abs(5);
(二)类模板
template<class/typename T>
class A
{
类成员声明
}
//类外定义成员函数
template<class/typename T> //模板参数表
类型名 类名<T>::函数名(参数表) //模板参数标识符列表
//建立对象
模板名<模板参数表>对象名1,...,对象名n;
//vector<int>a;