1、类、派生类
C++中类的概念很重要,重要到什么程度呢?把class/struct看成和main同一个等级,为什么这么说呢?因为,C++中不允许全局变量独立于类外,
所以,在一个C++文件中,除了头文件,就是class和main了。当然这只是其中一个原因。另外,类可以看做一种类型,和C中struct类似的一种类型。但又有一定的区别。下面具体分析。
不论是类还是派生类,都是类,具有类的属性。关于类,将从以下几个方面进行阐述:
(1)成员变量、成员函数
成员变量是与类相关的变量,成员函数是对成员变量操作的函数。关于成员变量和成员函数的认识,可以通过sizeof 来认识。sizeof是一个运算符,用于计算数据类型的大小。sizeof(int);int a = 4;sizeof a ;
a)空类、空结构体的大小为1个字节;如果只有成员函数,则还是只占用1个字节,因为类的成员函数独立于类的存储空间;
b)只含有虚函数的类的大小为4,或8(64位);
c)要注意const,static,它们存储在初始化数据区;
【总结】:类表面上包含成员函数和成员变量,但在其存储空间,只包含成员变量,成员函数独立于类的存储空间,存放在代码区。
(2)成员变量的访问方式
a、内部访问:由类中的成员函数对类的成员进行访问;(类访问)
b、外部访问:在类外,由类的对象对类的成员进行访问。(对象访问)
访问标号有三种,内部访问及友元函数可以访问类中所以成员;外部访问只能访问类的共有成员。
(3)static:作用是限定作用范围
【类外static】类外使用static其实就是C的范畴,和C中使用技巧一样。
A、static局部变量:在栈空间定义的,但是存在于全局变量区
和栈空间的局部变量比,其特点是:只初始化一次,存放地址不变,再次调用时,保持上一次的值不变。
B、static全局变量:以全局变量的方式定义,当然存在于全局变量区
和一般的全局变量相比,其特点是:不能用extern声明为引用,在其他文件中调用,保持上一次模块调用后的值
【类内static】C++特有
A、static成员变量:虽然定义在类内,但是存放在全局变量区,与对象无关,属于类;static成员变量要在类的定义外面初始化。
B、static成员函数:不含有this指针,所以不能调用本类的成员函数和成员变量,只能访问该类的静态成员变量,属于类。
此外,还要注意:
A、静态成员变量属于类,而不是属于某个特定的对象,它是由该类的所有对象共享的,因此不能在类的构造方法中初始化
B,静态成员属于该类所有对象公有,可以被类的对象调用,可以用类名直接调用
C,静态成员受private的限制
D,该类的静态函数只能访问该类的静态成员变量
E,常量成员才不能修改,静态成员变量必须初始化,但可以修改,如果没有显式初始化,会被程序自动初始化为0
(4)const:作用是防止一个变量值改变。
【类外const】
const变量,也称为只读变量,定义时,必须初始化。编译阶段做到只读。C的范畴
const对象, 只读对象,只能调用const型的成员,那么如何对这些成员进行初始化呢,答案是:初始化列表。C++范畴
【类内const】
const成员变量:在类中其实只是声明这个变量为const型,初始化工作在构造函数的初始化列表中进行。
不能再构造函数的函数体里对const变量进行赋值。
const成员函数:只读成员函数,该函数不能修改该对象的成员变量
构造函数、析构函数中不能使用const成员函数。
【const与函数】
const修饰形参,表示这个形参在改函数中不能进行修改。
const修饰返回值,表示这个反回值不能修改。
此外,要注意:
A,c++中,声明const int i,是在编译阶段做到 i只可读的。不可以修改。
const char str1[]=”abc”; const char str2[]=”abc”; const char *p1 = “abc”; const char *p2 = “abc”;
str1和str2地址不同,P1和P2地址相同
B,常量指针调只能用常量函数
class A { public: virtual void f() { cout<<"A::f()"<<endl; } void f() const { cout<<“A::f() const"<<endl; } } class B : A { public: void f(){ cout<<“B::f()”<<endl; } } g(const a* a) { a->f(); } int main() { A* a = new B(); a->f(); g(a); delete a ; }
C、引用:引用为对象起了另外的一个名字,该对象是已经存在的对象,引用必须初始化,有类型
(6)构造函数:作用就是为了方便进行类的成员变量的初始化 。所以在函数体里最好制作变量初始化相关的操作。
分为:无参构造函数(默认构造函数),有参构造函数,拷贝构造函数。在其函数体内可以进行:
A、初始化类的成员变量;
B、申请堆空间。
初始化时必须采用初始化列表一共有三种情况
A、继承来的对象(继承时调用基类构造函数)
B、const成员
C、引用成员
class A { public: A() { } ~A() { cout<<"~A"<<endl; } }; class B:public A { public: B(A &a):_a(a) { } ~B() { cout<<"~B"<<endl; } private: A _a; }; int main(void) { A a; //很简单,定义a的时候调用了一次构造函数 B b(a); }程序输出:~B ~A ~A ~A
注意两点:1)构造函数的调用顺序:类构造函数 > 子类成员变量构造函数 > 子类构造函数
析构函数与之相反
2)注意_a(a)是对B的私有成员_a的初始化
3)析构函数一般定义为虚函数,构造函数不能是虚函数
class A { ... private: int a; }; class B : public A { ... private: int a; public: const int b; A &c; static const char* d; B* e; }则构造函数中,成员变量一定要通过初始化列表来初始化的是b,c。分析:const成员要在构造函数中初始化。
class A { public: A() { printf(“0”); } A(int a) { printf(“1”); } A& operator=(const A& a) { printf(“2”); return*this; } } int main() { A al; al=10; }程序输出: 012分析: A a1; //调用A默认构造函数
a1=10; //类型不匹配,调用构造函数A(int)进行隐式转化,之后将引用传给operator=()
(7)析构函数:作用是对成员进行清理工作。
A、栈对象,编译器自动清理。在函数执行结束时,自动调用。
B、堆对象,new产生,必须手动清理,通常在析构函数中使用delete实现其析构。
C、全局对象,构造时先调用构造函数,析构时也会调用析构函数,但是程序中看不到调用析构函数。
(8)友元函数:
A,定义在类中的友元函数的作用域在全局作用域,在main函数中可以直接调用;
(9)内联函数inline
A,内联函数就是在编译阶段时,将程序中出现的内联函数的地方用内联函数的函数体来代替。因此,可以避免调用函数产生额外的时间开销,一般用于加快程序执行速度。
B,因为将代码嵌入到类中,所以可能导致可执行文件的变大或者变小。
C,在编译的时候做类型检查。
(10)虚函数
A,虚函数与普通函数的主要区别是具有运行时多态性,跟是否内联无关的。所以,虚函数可以是inline函数,加上inline后起不到inline的作用。
B,构造函数不能声明为虚函数
构造函数不能包含虚函数
析构函数可以声明为虚函数
析构函数不能包含任何函数,所以不能包含虚函数
C,虚函数表、虚表指针
有虚函数的类,前四个字节是虚表指针,指向虚表。
class Test{ public: int a; int b; virtual void fun() {} Test(int temp1 = 0, int temp2 = 0) { a=temp1 ; b=temp2 ; } int getA() { return a; } int getB() { return b; } }; int main() { Test obj(5, 10); // Changing a and b int* pInt = (int*)&obj; *(pInt+0) = 100; *(pInt+1) = 200; cout << "a = " << obj.getA() << endl; cout << "b = " << obj.getB() << endl; return 0; }代码输出:200 10
2、基类与派生类
(1)派生类对基类成员的访问形式:
a、内部访问:由派生类中新增的成员对基类继承来的成员的访问。
b、外部访问:在派生类外,由派生类的对象对从基类继承来的成员的访问。
(2)访问控制
基类成员在派生类的访问属性取决于继承方式以及这些成员本来在基类中的访问属性
a、基类的私有成员无论什么继承方式,在派生类中均不可以直接访问
b、在公有继承下,基类的保护成员和公有成员均保持原访问属性
c、在保护继承方式下,基类的保护和公有成员在派生类的访问属性均为保护属性
d、在私有继承下,基类的保护和公有成员在派生类中的访问属性均为私有属性
(3)构造函数的执行先执行父类,再执行子类。析构函数的析构顺序想反。
子类可以继承父类所有的成员变量和成员方法,但不继承父类的构造函数。因此,在创建子类对象时,为了初始化从父类继承来的数据成员, 系统需要调用其父类的构造方法。
3、模板
(1)编译时检查数据类型,保证了类型安全。
(2)函数模板必须由编译器根据程序员的调用类型实例化为可执行的函数。
4、运算符重载
友元函数运算符重载与成员运算符重载的区别是:友元函数没有this指针,而成员函数有,因此,在两个操作数的重载中友元函数有两个参
数,而成员函数只有一个。可以总结如下:
1.对双目运算符而言,成员函数重载运算符的函数参数表中只有一个参数,而用友元函数重载运算符函数参数表中含有两个参数。
对单目运算符来说,成员函数重载运算符的函数参数表中没有参数,而用友元函数重载运算符函数参数表中含有一个函数。这个问题要搞清
楚,有一个this指针的问题。。。
2.双目运算符一般可以用友元函数重载和成员函数重载,但有一种情况只可以用友元函数重载。即:双目运算符左边的变量是一个常量,而
不是对象!!!这点很重要的额。
而关于运算符的重载,记着:
1.对于单目运算符,建议选择成员函数;
2.对于运算符“=,(),[],->”只能作为成员函数;
3.对于运算符“+ =,-=,/=,*=,&=,!=,~=,%=,<<=,>>=”建议重载为成员函数;
4.对于其他运算符,建议重载为友元函数。
那么下面这个题的答案也就很明显了:
1、将x+y*z中的“+”用成员函数重载,“*”用友元函数重载应该写为:?
答案为:x.operator+(operator*(y,z))
2、若要重载+、=、<<、=和[]运算符,则必须作为类成员重载的运算符是哪些?
答案:=和[]