【结构体与类】:
结构体默认情况下,其成员是公有(public)的;
类默认情况下,其成员是私有(private)的。在一个类当中,公有成员是可以在类的外部进行访问的,而私有成员就只能在类的内部进行访问。
【构造函数】:
如果一个类中没有定义任何构造函数,那么编译器只有在以下三种情况下,才会提供默认的构造函数:
1、如果类有虚拟成员函数或者虚拟继承父亲(即有虚拟基类)时;
2、如果类的基类有构造函数(可以是用户定义的构造函数,或编译器提供的默认构造函数);
3、在类中的所有非晶态的对象数据成员,它们所属的类中有构造函数(可以是用户定义的构造函数,或者编译器提供的默认构造函数)。
综上:也就是说,当类的成员总能找到初始化的方法时。
【析构函数】;
不允许有返回值;
不允许带参数;
一个类中只能有一个析构函数;
当一个类的对象超出它的作用范围,对象所在的内存空间被系统回收;或delete删除对象时,析构函数自动调用。
【函数的重载】:
重载构成的条件:函数的参数类型、参数个数不同,才能构成函数的重载。 //编译器根据参数的类型和参数个数来确定执行哪一个构造函数。
在函数重载时,要注意函数带有默认参数的这种情况。
【this指针】:
this指针是一个隐含的指针,它是指向对象本身的,代表了对象的地址。一个类所有的对象调用的成员函数都是同一个代码段,那么,成员函数又是怎么识别属于不同对象的数据成员呢?
比如,在对象调用pt.input(10, 10)时,成员函数除了接收2个实参外,还接收到了pt对象的地址,这个地址被一个隐含的形参this指针所获取,它等同于执行this = &pt。所有对数据成员的访问都隐含地被加上了前缀this->。
class point
{
public:
int x;
int y;
void input(int x, int y) //这个成员函数的执行结果是:形参x值赋给了形参x,形参y的值赋给了形参y。因为,在input函数中,形参x和y的可见性覆盖了成员变量x和y。
{
x = x;
y = y;
}
void input_this(int x, int y) //赋值给了成员变量x和y
{
this->x = x;
this->y = y;
}
};
【继承】:
子类实例创建的时候,先调用父类的构造函数,再调用子类的构造函数。
子类实例创建的时候,如果不显示调用父类的构造函数,将默认调用父类的默认构造函数(不带参数的构造函数)。如果父类没有默认构造函数,将出错,因此,需要显示调用父类构造函数。如:fish():animal(10,29){}。
这种“:变量(参数)”的方式,还常用来对类中的常量(const)成员进行初始化。如point():x(0),y(0)。
类中普通的成员变量也可以采取此种方式进行初始化,然而,这就没必要了。
成员变量的权限:
public: 定义的成员可以在任何地方被访问。
protected: 定义的成员只能在该类及其子类中访问。
private: 定义的成员只能在该类自身中访问。
继承的权限:
public:
protected:
private: 如果在定义派生类时没有指定如何继承访问权限,则默认为private.
基类中的成员在派生类中的访问权限由,以上二者权限“与”操作得来。
即,public && (public | protected | private) = public | protected | private
protected && (public | protected | private) = protected | protected | private
private && (public | protected | private) = private | private | private
另外,基类的private成员不能被派生类访问,因此,private成员不能被派生类所继承
因此,如上的“与”操作应修改为:
public && (public | protected | --) = public | protected | --
protected && (public | protected | --) = protected | protected | --
private && (public | protected | --) = private | private | --
多重继承:一个类可以从多个基类中派生。如:class B : public C, public D {}
初始化是按基类表(继承的出现顺序)中的说明顺序进行初始化的。
因此,构造函数的调用顺序需要注意;析构函数按基类表说明顺序的反向进行调用,也需要注意。
【虚函数与多态性、纯虚函数】:这节全部抄下来了,因为重要。
1、虚函数与多态性
因为鱼的呼吸是吐泡泡,和一般动物的呼吸不太一样,所以我们在fish类中重新定义breathe方法。我们希望如果对象是鱼,就调用fish类的breathe()方法,如果对象是动物,那么就调用animal类的breathe()方法。程序代码如下所示。
#include <iostream.h>
class animal
{
public:
void breathe(){cout << "animal breathe" << endl;}
};
class fish : public animal
{
public:
void breathe(){cout << "fish bubble" << endl;}
};
void fn(animal *pAn) {pAn->breathe();}
void main()
{
animal *pAn;
fish fh;
pAn = &fh;
fn(pAn);
}
我们在fish类中重新定义了breathe()方法,采用吐泡泡的方式进行呼吸。接着定义了一个全局函数fn(),指向animal类的指针作为fn()函数的参数。
在main()函数中,定义了一个fish类的对象,将它的地址赋给了指向animal类的指针变量pAn,然后调用fn()函数。
虽然C++是强类型的语言,对类型的检查是非常严格的,但将fish对象fh的地址直接赋给指向animal类的指针变量,C++编译器是不报错的。
这是因为fish对象也是一个animal对象,将fish类型转换为animal类型不用强制类型转换,C++编译器会自动进行这种转换。
反过来,则不能把animal对象看成是fish对象,如果一个animal对象确实是fish对象,那么在程序中需要进行强制类型转换,这样编译才不报错。
用内存操作来解释上面的现象:
当构造fish类对象时,首先要调用animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完成自身部分的构造,从而拼接出一个完整的fish对象。
animal类对象所占内存: -->|___animal的对象所占内存___________|
fish类对象所占内存: -->|___animal的对象所占内存___________|____fish的对象自身增加的部分_____|
当把fish对象的指针赋值给animal对象指针时,指针的访问范围在animal对象内存范围内,不会越界,编译也不报错;
当把animal对象的指针赋值给fish对象指针时,因为fish对象指针的访问范围大于animal对象的内存范围,也许会越界,因为,编译会报错;
当一个animal对象确实是fish对象时,也就是说这个animal对象确实包含“animal对象内存”和“fish对象增加内存”两部分的时候,需要用强制类型转换,以使编译不报错。
显然,上面程序的输出结果为:animal breathe
现在,在animal类的breathe()方法前面加上一个virtual关键字:
#include <iostream.h>
class animal
{
public:
virtual void breathe(){cout << "animal breathe" << endl;}
};
class fish : public animal
{
public:
void breathe(){cout << "fish bubble" << endl;}
};
void fn(animal *pAn) {pAn->breathe();}
void main()
{
animal *pAn;
fish fh;
pAn = &fh;
fn(pAn);
}
用virtual关键字申明的函数叫做虚函数。程序结果为:fish bubble
这就是C++中的多态性。当C++编译器在编译的时候,发现animal类的breathe()函数是虚函数,这是C++就会采用迟绑定(late binding)技术。
也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型(在程序中,我们传递的fish类对象的地址)来确认调用的是哪一个函数,这种能力就叫做C++的多态性。
我们没有在breathe()函数前加virtual关键字时,C++编译器在编译时就确定了哪个函数被调用,这叫做早期绑定(early binding)。
注:也就是说,编译器的原则是:能确定的就先确定,不确定的就在运行时再确定。
C++的多态性是通过迟绑定技术来实现的。
即,在积累的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
2、纯虚函数
将breathe()函数申明为纯虚函数:
class animal
{
public:
virtual void breathe() = 0;
};
纯虚函数是指被表明为不具体实现的虚成员函数(注意:纯虚函数也可以有函数体,但这种提供函数体的用法很少见)。
纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再具体地给出定义。
凡是含有纯虚函数的类叫做抽象类。这种类不能声明对象,只是作为基类为派生类服务。
在派生类中必须完全实现基类的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。
纯虚函数多用在一些方法行为的设计上。在设计基类时,不太好确定或将来的行为多种多样,而此行为又是必须的,我们就可以在基类的设计中,以纯虚函数来声明此种行为,而不具体实现它。
注意:C++的多态性是由虚函数来实现的,而不是纯虚函数。在子类中如果有对基类虚函数的覆盖定义,无论该覆盖定义是否有virtual关键字,都是虚函数。
笔者注:多态是由虚函数实现的,虚函数是迟绑定的,也就是说,可以根据具体对象类型来确定访问哪个函数。
如果,上层类定义了某函数为虚函数,下层类对该函数的定义也都成了虚函数,必须的!
也就是说,虚函数只与绑定时间相关,与内存占用特点无关。虚函数和普通函数一样占用内存,只是绑定时间不同。
问:纯虚函数在对象实体(内存)中有分配内存吗?占多少呢?
【函数的覆盖和隐藏】
1、函数的覆盖(override)
构成函数覆盖的条件:
基类函数必须是虚函数(virtual);
发生覆盖的两个函数要分别位于派生类和基类中;
函数名称与参数列表必须完全相同。
由于C++的多态性是通过虚函数来实现的,所以函数的覆盖总是和多态关联在一起。在函数覆盖的情况下,编译器会再运行时根据对想想的实际类型来确定要调用的函数。
2、函数的隐藏
构成函数隐藏的条件:
基类的函数不是虚函数;
派生类中具有与基类同名的函数(不考虑参数列表是否相同);
从而在派生类中隐藏了基类的同名函数。
函数的隐藏、覆盖、重载:
(1)派生类的函数与基类的函数完全相同(函数名和参数列表都相同),只是基类的函数没有使用virtual关键字。此时基类的函数将被隐藏,而不是覆盖。
(2)派生类的函数与基类的函数同名,但参数列表不同,此时,不管基类的函数声明是否有virtual关键字,基类的函数都降被隐藏。
(3)重载发生在同一个类中。
总结:重载发生在同一个类中。覆盖和隐藏发生在父子两个类中;当父类用virtual,名称和参数列表完全相同时,为覆盖;其余为隐藏。
考察如下程序:
#include <iostream.h>
class Base
{
public:
virtual void xfn(int i)
{
cout << "Base::xfn(int i)" << endl;
}
void yfn(float f)
{
cout << "Base::yfn(float f)" << endl;
}
void zfn()
{
cout << "Base::zfn()" << endl;
}
};
class Derived : public Base
{
public:
void xfn(int i) //覆盖了基类的xfn函数
{
cout << "Drived::xfn(int i)" << endl;
}
void yfn(int c) //隐藏了基类的yfn函数
{
cout << "Drived::yfn(int c)" << endl;
}
void zfn() //隐藏了基类的zfn函数
{
cout << "Drived::zfn()" << endl;
}
};
void main()
{
Derived d;
Base *pB = &d;
Derived *pD = &d;
pB->xfn(5);
pD->xfn(5);
pB->yfn(3.14f);
pD->yfn(3.14f);
pB->zfn();
pD->zfn();
}
结果:
Drived::xfn(int i)
Drived::xfn(int i)
Base::yfn(float f)
Drived::yfn(int c)
Base::zfn()
Derived::zfn()
总结:当基类函数为virtual函数,基类与派生类函数名称、参数列表完全相同时,为覆盖;其他情况为隐藏。
对于函数覆盖,迟绑定,也就是说,对象是基类对象则执行基类函数,对象是派生类对象则执行派生类函数。这个过程,就像是:用实际对象的函数覆盖了继承相关的其他该函数。
所以,不管指针是基类指针还是继承类指针,调用的函数都是实际对象的函数,即覆盖。
对于函数隐藏,基类和派生类的各自的函数都还在,所以,可以通过不同类指针访问到。
另外,在派生类中,被覆盖的基类虚函数是不可见的;当隐藏发生时,如果在派生类的同名函数中想要调用基类的被隐藏函数,可以使用“基类名::函数名(参数)”的语法形式。
另外,类的继承,可以看成是在基类的内存上一层一层加,如果在上层有同名同参的虚函数,那么在下层的函数也为虚函数,并发生函数覆盖:在一层看不到另一层的同名同参虚函数。
【引用】
引用:一个变来那个的别名,需要用另一个变来那个或对象来初始化自身。
引用和变来那个指向同一块内存,因此通过引用或变量可以改变同一块内存中的内用。引用一旦初始化,它就代表了一块特定的内存,再也不能代表其他的内存。
int a = 5; int &b = a; //b是a的别名,b再也不能再成为其他变量的别名
int c = 3; b = c; //b代表的a被赋值为3
引用和指针变量的区别:
引用只是一个别名,是一个变量或对象的替换名称。引用的地址没有任何意义,因此C++没有提供访问引用本身地址的方法。
引用的地址就是它所引用的变来那个或者对象的地址,对引用的地址所做的操作就是对被引用的变量或对象的地址所做的操作。
如:int a = 5; int &b = a; // 0x0012ff7c -->|__a=5__|, &a=0x0012ff7c, &b = 0x0012ff7c, a=5, b=5
int a = 5; int *pA = &a; // 0x0012ff7c -->|__a=5__|, 0x0012ff78 -->|__pA=0012ff7c__|, &a=0x0012ff7c, pA=0x0012ff7c, &pA=0x0012ff78, a=5, *pA=5
引用多树用在函数的形参定义上,在调用函数传参时,经常使用指针传递,一是避免在实参占较大内存时发生值的复制,二是完成一些特殊的作用,例如,要在函数中修改实参所指向内存中的内容。
同样,使用引用作为函数的形参也能完成指针的功能,在有些情况下还能达到比使用指针更好的效果。
例如:
//change函数用来交换a和b的值
void change(int& a, int& b)
{
a = a + b;
b = a - b;
a = a - b;
}
调用的时候使用change(x,y); 如果使用指针传递,调用则为change(&x, &y),这让人迷惑,不知道交换的是x和y的值,还是x和y的地址。此处用引用,可读性比指针更好。