C++类
一.class与struct区别:
1.struct的成员默认权限是public,而class的成员默认权限是private。2.struct的默认继承方式为public,而class的默认继承为private
二.类定义
class Date
{
int year,month,day;
public:
void Set(int y,int m,int d);
bool IsLeapYear();
void Print();
};
void Date::Set(int y,int m,int d)
{
year = y;
month = m;
day = d;
}
bool Date::IsLeapYear()
{
return(year%4 == 0 && year%100 !=0) || (year%400 == 0);
}
void Date::Print()
{
cout<<year<<month<<day;
}
int main()
{
Date d;
d.Set(2000,12,6);
if(d.IsLeapYear())
d.Print();
}
1.成员函数
成员函数定义可以放在类定义中,这时函数名前可以不冠有类名了。只要是在类定义中包含的成员函数,就有默认声明内联的性质。也就是说,该成员函数处于被编译自动安排到最佳运行性能的状态。另一方面,把成员函数写入类定义中,就使得类定义体中的大小不可预见。因此,类定义体作为应用程序员参考的意义会因为参考文档不必要的庞大而受到伤害,而作为类的实现者也一定程度地失去了工作意义,似乎类定义工作,已经包含类的实现了。而且,编译是否真的将成员函数安排为内联,还要看函数是否足够简单,是否包含不适于内联运行的循环结构。所以,就尽量将成员函数的定义写到类定义的外部去。而内联函数另外的体现就是在类内声明,在定义的时候在函数前加入inline关键字。
2.使用对象指针
调用成员函数3种形式
(1)objectName.memberFunctionName(Parameters)
(2)ObjectPoniter->memberFunctionName(Parameters)
(3)(*ObjectPoniter).memberFunctionName(Parameters) //点操作符的优先级大于*号
3.常成员函数
如果只对对象进行读操作,则该成员函数可以设计成常成员函数。好处就是让使用者一目了然地知道该成员函数不会改变对象值。同时让类的实现者更方便的调试,因为在常成员函数中,任何改变对象值的操作,都将被编译器认定为错误。更方便软件设计遇控制软件质量,尽量不让类产品有意外的失措。所以,能够成为常成员函数的,应尽量写成常成员函数形式。要注意的是,在函数的声明和定义处都使用const,如果有一处省略,则代码将不能被编译。
4.重载成员函数
成员函数跟普通函数一样,也可以重载。对重载的识别和使用规则也是相同的。
5.常对象
修饰符const不仅可以用于修饰基本数据类型,也可以修饰类对象。类的成员函数有一个默认实参this,代表当前类对象,const就是修饰这个实参的,这意味着我们不能在函数内修改该类对象的数据成员,或调用非const成员函数。
6.限制创建类对象的基本原理:构造函数私有化;提供自己的静态构造函数
class MyClass
{
public:
static MyClass* CreateInstance(); //静态构造函数
{
if(NULL == m_pInstance)
{
m_pInstance = new MyClass;
}
return m_pInstance;
}
private:
MyClass(){} //私有化构造函数
static MyClass* m_pInstance;
};
MyClass* MyClass::m_pInstance = NULL;
所以程序只能通过调用CreateInstance()函数创建MyClass类对象,例如
MyClass Obj();
获得指向类对象的正确方法
MyClass* pObj1 = MyClass::CreateInstance();
MyClass* pObj2 = MyClass::CreateInstance(); //不会创建新的MyClass对象
三.重载操作符
1.运算符重载特征
C++其实把操作符也看成是与函数同样性质的实体了。因此,可以对操作符进行函数那样的定义,之后,就可以自由地使用该操作符了。重载操作符是C++的一个非常重要的特性,因为它使我们能够使用像+,-,*,=这样的标准C++运算符来处理自定义数据类型的对象。C++规定了一些不能重载的运算符,如:成员访问运算符".",作用域运算符"::",指针间接运算符".",间接成员访问运算符"->",条件运算符"?:"。如果不为类提供重载的赋值运算符,则编译器会提供一个默认的赋值运算符函数,它也是对对象的成员进行逐个复制。需要注意,复制构造函数与重载赋值运算符是不同的,复制构造函数是通过声明以现有同类对象进行初始化的类对象,或者通过以传值方式给函数传递对象而被调用。赋值运算符是在赋值语句中被调用。
//01.cpp
01.#include<iostream>
02.using namespace std;
03.class Point
04.{
05. int x,y;
06. public:
07. void set(int a,int b){x = a;y = b;}
08. void print() const{cout<<"("<<x<<","<<y<<")\n";}
09. friend Point operator+(const Point& a,const Point& b);
10. friend Point add(const Point& a,const Point& b);
11.};
12.Point operator+(const Point& a,const Point& b)
13.{
14. Point s;
15. s.set(a.x+b.x,a.y+b.y);
16. return s;
17.}
18.Point add(const Point& a,const Point& b)
19.{
20. Point s;
21. s.set(a.x+b.x,a.y+b.y);
22. return s;
23.}
24.int main()
25.{
26. Point a,b;
27. a.set(3,2);
28. b.set(1,5);
29. (a+b).print();
30. operator+(a,b).print();
31. add(a,b).print();
32}
在进行a=b的操作时,C++实际上调用了函数operator+(a,b),即第9行声明。对于双目操作,人们习惯于将操作符放在两个操作数的中间,即表达式的中缀表达法,所以C++采用了人性化的设计,将a+b转译为对operator+(a,b)的调用。如果不用操作符,a+b的功能也是可以实现的,那就是要调用add函数。从程序中来看,操作符不是必须的,而是为了在编程中进行人性化描述,达到更好理解程序的目的。
2.性质
1.拒绝新创
不能创建新的操作符。例如,@不是C++的运算符,**也不是,不能定义它们为C++的操作符
2.个别重载限制
C++还规定双目操作符"::","."和"*"不能重载,因为它们都特殊的要求第二参数必须为名称,所以,对操作数随意性表达的伤害限制了它们。如果重载这些操作符,就有可能改变C++的语法结构。此外,三目运算符,以及sizeof,typeof也不允许重载。
3.优先级和结合性不变
C++的操作符都是有优先级和结合性的,重载操作符后,其优先级和结合性是不会改变的。例如,如果定义了Point类型的+和*操作,则对于:
Point a,b,c;
Point d = a+b*c;
一定是先做b*c,然后再做+操作,最后把结果赋给d
4.操作数个数不变
原先的操作符是单目的,重载也是单止形式,原先操作符是双目的,重载也是双目的,这是不能改变的。例如,++是单目运算符,你就不能重载两个参数的++操作符 Point operator++(Point a,Point b)
5.专门处理对象
操作符的重载只能针对自定义类型,即在操作符定义的参数表中,至少有一个参数必须是自定义类型。因为任何内部数据类型的操作符默认定义。C++自认为已经完善,不允许编程者对C++内部系统重新定义。例如,double是内部数据类型,本来它是不能进行%操作的,因此,现在也不能声明和定义以double为参数的%操作。
double operator%(double a,double b);//错
假如允许上面的操作符生效,那么就使表达式3.5 % 6.7操作通过编译。你虽然实现了一个自定义的%操作,实现了本可以通过其他函数定义处理的操作,当你自鸣得意的同时,却无意地招致编译在别处对错误表达式的合法理解!例如表达式“a*b+c%d;”,原先,由于d是浮点型变量,编译能够帮助你检查出错误的,现在却不能帮你了,而且还错误地挂上了你自己定义的%操作。操作符重载针对自定义类型的性质,使得操作符重载专门用于类对象的操作。
6.忌意义相左
操作符重载后,就给操作符赋予了新的意义,新的意义应该反映操作的本质。例如,我们如果定义矩阵加法的操作符*,而定义矩阵乘法操作为<<,那么:
Matrix a,b;
a = a*b;
a = a<<b;
编程在理解上就会受到极大的伤害,还不如不重载操作符,直接定义函数的好。
四.访问控制
在类中出现的private,protected,public都是访问控制符。成员一旦定性为private,外界就不能直接访问,而只能通过类的内部成员去间接访问。而类中的操作,一般是作为类的资源提供给类的使用者的,使用者可以通过对象名的捆绑而调用成员函数,所以成员函数作为对类对象的整体性操作,其本质上应该是公有的。
类通过访问控制符public. private,来分隔提供给用户的操作(公有)与内部实现的细节(私有)。public属下的成员是提供给用户用的,private属下的成员是类内部实现中所需的数据组成和操作,它们与外界隔绝。
一个类,自从由其创建对象之后,便通过对象的若干操作来满足所需问题求解的要求。这也是一个数据类型的本质。因此,类提供通常的操作给使用者,除此之外,应该尽量封闭自己。这就像电视机,除了提供给用户的按钮操作之外,其他内部电路的细节统统封闭在电视机外壳之内。这样的类机制设计是自然的、合理的。因为用户用不到的操作,如果暴露在用户看得见够得着的地方,便会造成一些安全隐患。
可以把类定义中的公有成员函数看作是使用类的一个说明。因为有了类公有成员函数声明,就可以使用类了,就可以编译通过所有使用该类的编程了。类定义的这个性质,很像电视机的使用说明书,说明电视机操作按钮的使用方法。类定义的说明书性质是使用类的程序员(包括设计类的程序员)与类定义代码之间的一种关系。而描述类定义代码和类的应用代码及类的实现代码之间的关系时,则称其为类的界面,或称类的接口。
类定义的界面特性,很自然地便可将其写成头文件的形式。而类的实现则是一个编译单位,使用类的代码放在其他编译单位中。它们相互独立,又同属于一个程序体。
如果是定义内联成员函数,也必须将该成员函数的定义写在头文件中,包含在头文件之内。由于内联函数一般都很小,所以一般都是在类内定义的,没有必要都己经写在头文件中了,还要写到类定义外去实现。
1.静态数据成员
有一些属性不是类中每个对象分别拥有的,而是共有的。这些共有的属性有些是变化的,如类对象创建的计数值,有些是不变的,如日期类中要用到的12个月的名称,它是一个数组。这些属性不应该作为全局变量,因为它们是专属于某个类的,而不是属于过眼烟云的程序。类是可以反复使用的模块,它有鲜明的大众性和服务性。而程序只是为了某个特定目的,具有时间上的局限性,完成了使命,就失去了存在的价值。这些属性也不应该是数据成员,因为不能让每个对象都单独拥有它。
(1)#include<iostream>
(2)u.ing namespace std;
(3)class.Student{
(4) static int number;
(5)string name;
(6)public,
(7)void set(string str){
(8)name=str;
(9)++number;
(10)}
(11) void print(){cout<<name<<"一>students are "<<number<<" numbers\n";}
(12)};//一一一一一一一一一一一一一一一一一一一一-一一一一一一--一一一一一
(13) int Student::number = 0;//静态数据成员在类外分配空间和初始化
(14)//--一一---一---~--一一一一一一一一~一一一一一-一~一-一
(15}void fn(){
(16) student S1;
(17)s1.set(”Jenny”);
(18)Student s2;
(19)s2.set(”Rand”);
(20)s1 .prirnt();
(21)}
(22)int main(){
(23)Student s;
(24)s.set(”Smith”);
(25)fn();
(26)s.print();
(27)}
输出:Jenny->student are 3 numbers
Smith->student are 3 numbers
将数据成员设计成静态数据成员后,该成员的变化仍然会在每次对象创建之后的名称赋值中反映出来。反映的结果存放在专属于Student类名空间的全局数据区中,不属于各个Student对象。整个类中只有一份number拷贝,所有的对象都共享这份拷贝。因此,输出学生总数时,访问的是惟一的静态数据成员,它不会因对象而异了。无论第20行还是第25行,输出曾经创建的对象总数就正确了。由于静态成员脱离对象而存在的性质,所以该实体应在所有对象产生之前存在,因此,更适当的时机是在程序启动的时候,做其初始化。第13行便是其初始化的语句,似乎像个全局变量,但它属于Student类名空间。
该实体在程序中的惟一性,要求其不能跟着Student类定义放在头文件中,但它又确实是Student类的一员,所以,放在类的实现代码中是最合适的。定义静态成员的格式不能重复static关键字(第13行),但必须在成员名前冠以类名加域操作符,以表示该成员的类属。如果不将其初始化,则系统将为该成员清0。静态成员是不能用构造函数初始化的,因为静态成员变量属于整个类,它是在编译时分配内存,而类对象在程序运行时是可以多次创建的。
2.静态成员函数
在类中声明静态成员函数,要在成员函数名前加上关键字static。静态成员函数并不受对象的牵制,可以用对象名调用静态成员函数,也可以用类名加上域操作符调用静态成员函数,这时候,将它看做是某个名空间的一个函数。静态成员函数的实现位置与成员函数的实现位置该是在一起的,静态成员函数如果不在类中实现,而在类的外部实现时,类名前应免去static关键字。成员函数的静态性只在第一次声明的时候才是必要的。因为静态成员函数可以不以捆绑对象的形式调用,静态成员函数被调用时,没有当前对象的信息,所以静态成员函数不能访问数据成员.
五.构造函数
1.类的构造函数是类的一个特殊函数,它在创建类的对象被自动调用。因此,可以在构造函数中对类的数据成员进行赋值,实现在创建类的对象时对各数据成员进行有效初始化。C++规定,与类同名的成员函数就是构造函数,构造函数没有任何返回类型,即使写为void也是不允许的。构造函数是由编译器自动调用,不要在创建对象后试图手动调用构造函数。在类的实例进入其作用域时,也就是建立了一个对象,构造函数就会被调用 ,那么构造函数的作用是什么呢?当建立一个对象时,常常需要做某些初始化的工作。例如,对数据成员进行赋值,设置类的属性,这些操作刚好放在构造函数中完成。
无参构造函数也称默认构造函数(default constructor,因为在描述上涉及默认的默认构造函数(default default constructor),可能会引起不必要的混乱,而无参构造函数能更精确地描述汉语的语义。如果手工定义了无参构造函数,或者任何其他的构造函数,则系统不再提供默认的无参构造函数。
类定义中有数据成员和成员函数,数据成员可以是内部数据类型的变量实体,也可以是对象实体。例如,有一个学号类和一个学生类,学生类中包含了学号类的对象,因此在构造学生对象时,面临着学号类对象的构造:
#include<iostream>
(06)using namespace std;
(08)class StudentID{
(09)int value;
(10)public:
(a)StudentID(){
(12)static int nextStudentID = 0;
(13)value = ++nextStudentID;
(14)cout<<"Assigning student id "<<value<<”\n”;
(15)}
(16)};
(0)class Student{
(18)string name;
(19)StudentID id;
(20)public:
(21)Student(string n=”noName”){
(22)cout<<”Constructing student ”+n+”\n”;
(23)name=n;
(24)}
(25)};
(26)int main()(
(27)Student s(”Randy”);
(28}
输出:
Assigning student id 1
Constructing student Randy
可是,在学生类的构造函数中并没有看到学号类对象初始化的痕迹,而数据成员name倒被赋了初值。从运行结果看,当学生类对象被构造时,一个学号对象也创建了,而且学号类的构造函数先于学生类对象的构造函数体的执行而执行。C++的类机制对于含有对象成员的类对象的构造定了一些规则,对于第27行语句,其内部的执行次序是这样的:
(1)先分配学生类对象S的空间,调用Student构造函数;
(2)在Student构造函数体尚未执行时,由于看到了类的对象成员id,转而去调用学号类的无参构造函数;相当于去执行定义语句StudentID id;(3)执行了学号类构造函数体,输出结果的第1行信息,返回到Student构造函数;
(4)执行Student构造函数体,输出结果的第2行信息,完成全部构造工作。
这里对学号类构造函数的调用是默认的,默认调用便是调用无参构造函数,而正好学号类设计的构造函数就是无参构造函数。
2.复制构造函数
复制构造函数就是从通过一个对象来创建另一个对象。如果我们没有为类添加复制构造函数,则编译器也会添加一个默认复制构造函数。默认复制构造函数只是将原来对象的数据成员中存储的数值复制到新对象中。这样,当类内部使用指针进行动态内存分配时,只是将原来类对象的指针成员存储的地址复制到新对象中,即两个对象共享的内存。这会导致严重的错误,当一个对象被销毁时,则另一个对象中的指针将指向已经释放的未知内存,因此会造成严重错误。解决办法就是提供一个复制构造函数。复制构造函数的参数不能是对象:因为复制构造函数是在使用现有对象创建被初始化新对象,以及以对象为参数进行传递,又或者函数以对象为返回值的时候调用的。只要涉及这三个形式,系统就是调用复制构造函数。如果在复制构造函数中以对象为参数,则这会导致调用复制构造函数,复制构造函数调用复制构造函数就会形成无穷的死循环。在开发程序中可能需要保存对象的副本,以便在以后执行的过程中恢复对象的状态。那么如何用一个已经初始化的对象来新生成一个一模一样的对象?答案是使用复制构造函数来实现。复制构造函数就是函数的参数是一个已经初始化的类对象。(见深浅拷贝)
六.构造顺序
1.局部对象
在C中,所有的局部变量(没有对象)都是在函数开始执行时统一创建的,创建的顺序是根据变量在程序中按语句行出现的顺序。而C++却不同,它是根据运行中定义对象的顺序来决定对象创建的顺序。而且,静态对象只创建一次。
2.全局对象
构造全局对象不像局部对象那么简单,全局对象没有明确的控制流来表明其顺序。因为全局对象还有多个源文件之间的协调问题,多文件的程序只有等到程序链接之后相互之间的关系才能搞定,但程序文件相互关系确定并不等于对象创建顺序确定,事实上,全局对象的创建顺序是编译器编造出来的,因而不同的编译器做法就不同。
八.析构函数
构造函数和析构函数是类体定义中比较特殊的两个函数。因为他们两个都没有返回值,而且函数名标识符和类名标识符相同,析构函数名标识符就是在类名标识符前面加"~"符号。
构造函数主要是用来在对象创建时,给对象中的一引起成员赋值,主要目的就是来初始化对象。析构函数的功能是释放一个对象的,在对象删除前,用它来做一些清理工作,它与构造函数的功能正好相反。当对象不再有效时,程序将自动调用类的析构函数,以释放创建对象时分配的资源。析构函数不能重载。
不能手动调用析构函数,析构函数是在对象被撤销时被调用,不是调用了析构函数就撤销了对象。显式的调用析构函数是一件非常危险的事情,因为如果系统会自动调用析构函数,无论我们自己是否已经调用过,仍然会再次调用。换句话说,我们自己所谓的显式调用析构函数,实际上只是调用了一个普通成员函数,并没有真正意义上的让对象"析构"。
关于析构函数的说明:
1.当程序的执行离开对象的作用域后,对象就会被撤销,析构函数会被调用
2.析构函数本身并不释放对象占用的内存空间,它只是在系统收回对象时用于清除该对象分配的一些资源,如果该对象没有申请资源,也就不需要delete语句。
3.和构造函数一样,每个类都有一个析构函数且只请允许有一个析构函数。如果多了编译器就无法确定调用哪一个析构函数。构造函数有多个,因为构造函数调用时编译器可以根据变量的类型和数量来确定调用哪个构造函数。而析构函数是系统调用的,不能做到这种效果。不用写析构函数是因为编译器为类生成了一个默认的析构函数,而这个析构函数只是简单地执行它的任务。对于某些类中,由于包含动态分配的内存或其他资源,默认的析构函数无法释放资源时,这个时候需要用户自己写析构函数释放资源。
使用析构函数注意事项:
1.一个类中只可能定义一个析构函数
2.析构函数不能重载
3.构造函数和析构函数不能使用return语句返回值,不用加上关键字void
构造函数与析构函数的调用环境:
1.自动变量的作用域是某个模块,当此模块被激活时,自动变量调用构造函数,当退出些模块时,会自动调用析构函数。
2.全局函数在进入main之前会调用构造函数,在程序中止时会调用析构函数
3.动态分配的对象在使用new为对象分配内存时会调用构造函数,使用delete删除对象时会调用析构函数
4.临时变量是为支持计算,由编译器自动产生,临时变量的生存期的开始和结尾会调用构造函数和析构函数。
class对象大小与什么有关系
对于一个空类,其大小为1,而不是0。每个实例在内存中都有一个独一无二的地址,为了达到这个结果,编译器往往会给一个空类隐含的加一字节,这样空类在实例化后在内存中就得到了独一无二的地址。所以sizeof(class)的大小为1
class CWnd
{
public:
CWnd();
~CWnd();
char* GetTitle(){return m_szTitle}
private:
char m_szTitle[25];
} //在VS2008中sizeof(CWnd)结果为25,VS2010中为28
无虚函数类对象大小影响因素
class对象非静态数据成员占用内存大小会影响类对象大小
class对象采用的内存对齐策略会影响类对象大小
class对象中函数成员不会影响对象大小
有虚函数表情况
class对象非静态数据成员占用内存大小会影响类对象大小
class对象采用的内存对齐策略会影响类对象大小
class对象中普通函数成员不会影响对象大小,但class中的virtual函数会影响类对象大小。因为Virtrual-Table的原因虚函数占4个字节空间
虚函数表是一个函数指针数组,在C++类对象中仅存储函数指针数组的首地址。当函数通过虚函数实现调用时,类对象通过查询虚函数表决定哪个函数应该被调用,然后调用此函数完成函数调用执行。
虚函数表生成条件:只要类声明时包含virtual函数,类对象创建就会生成此对象的虚函数表,并把此虚函数表的首地址记录到此对象中。
class A class B:public A class C:public A class D:public B,public C
{ { { {
public: public: public: public:
virtual void f(){} virtual void f(){} virtual void f(){} virtual void f(){}
}; virtual void h(){} virtual void g(){} virtual void g(){}
}; }; virtual void h(){}
};
void main()
{
cout<<"sizeof(A)="<<sizeof(A)<<endl;
cout<<"sizeof(B)="<<sizeof(B)<<endl;
cout<<"sizeof(C)="<<sizeof(C)<<endl;
cout<<"sizeof(D)="<<sizeof(D)<<endl;
}
输出结果:sizeof(A)=4 sizeof(B)=4 sizeof(C)=4 sizeof(D)=8
在类的设计中,类对象的大小并不仅仅是对象中非静态数据成员的总和,其它因素包括内存对齐,虚函数,虚继承都影响类对象的大小,但不包括静态数据成员,因为它分布于全局存储区域,不占用类对象的空间