类和对象2
一、构造函数
1、对象的初始化
*类的数据成员是不能在声明类时初始化的,因为类并不是实体,而是一种抽象类型,并不占用存储空间,所以也无处容纳数据。
**当类的所有数据成员都是公共的时可以类似结构体一样的在定义对象的时候进行赋值,但是如果有私有的private或protected时,就变的很复杂了,因此引用了构造函数。
2、作用
C++提供构造函数来解决对象的初始化。它是一种特殊的成员函数,构造函数的名字必须与类名同名。在建立对象时自动执行构造函数。
#include <iostream>
#include <string>
using namespace std;
class Time //声明结构体类型Student
{public:
Time() //定义构造成员函数,函数名与类名相同
{hour = 0; //利用构造函数对象中的数据成员赋值
minute = 0;
sec = 0;
}
void set_time(); //函数声明
void show_time(); //函数声明
private:
int hour; //私有数据成员
int minute;
int sec;
};
void Time::set_time()
{
cin>>hour;
cin>>minute;
cinn>>sec;
}
void Time::show_time()
{
cout <<hour<<":"<<minute<<":"<<sec<<endl;
}
int main()
{
Time t1; //建立对象t1,同时调用构造函数t1.Time()
t1.set_time(); //对t1的数据成员赋值
t1.show_time(); //显示
Time t2;
t2.show_time();
return 0;
}
还可以在类体内声明,类外定义构造函数。
Time();//在类内进行构造函数声明
Time::Time() //在类外定义构造函数,加上类名和作用域限制符::
{hour = 0;
minute = 0;
sec = 0;
}
*构造函数不需要用户调用,也不能被调用,建立对象时由系统自动执行。
3、代参数的构造函数
为了解决给不同的对象赋予不同的初值,可以采用带参数的构造函数。*实参是在定义对象时给出的,一般格式为:
构造函数名(类型1 形参1,类型2 形参2,…)
定义对象时一般格式为:
类名 对象名(实参1,实参2,…)
如
#include <iostream>
#include <string>
using namespace std;
class Box
{public:
Box(int,int,int); //声明带参数的构造成员函数,函数名与类名相同
int volume(); //声明计算体积的函数
private:
int heigh;
int width;
int length;
};
Box::Box(int h,int w,int len)//在类外定义带参数的构造成员函数
{
heigh = h;
width = w;
length = len;
}
int Box::volume() //定义计算体积的函数
{
return(heigh*width*length);
}
int main()
{
Box box1(12,10,15); //建立对象box1,同时指定初值
cout <<"The volume of box1 is"<<box1.volume()<<endl;
Box box2(14,10,20); //建立对象box2,同时指定初值
cout <<"The volume of box2 is"<<box2.volume()<<endl;
return 0;
}

*C++还提供了一种初始化数据成员的方法——参数初始化表,在函数首部实现:
Box::Box(int h,int w,int len):height(h),width(w),lenght(len){}
这种写法简单便捷,甚至可以在类中定义构造函数。
4、构造函数的重载
函数的重构依然适用于构造函数,函数名一样,里面的参数个数和类型不一样,这样就可以提供不同的初始化。
#include <iostream>
#include <string>
using namespace std;
class Box
{public:
Box(); //声明一个无参的构造函数
Box(int,int,int):height(h),width(w),length(len){} //声明带参数的构造成员函数,使用初始化表
int volume(); //声明计算体积的函数
private:
int heigh;
int width;
int length;
};
Box::Box()//在类外定义无参的构造成员函数
{
heigh = 10;
width = 10;
length = 10;
}
int Box::volume() //定义计算体积的函数
{
return(heigh*width*length);
}
int main()
{
Box box1; //建立对象box1,不指定实参
cout <<"The volume of box1 is"<<box1.volume()<<endl;
Box box2(14,10,20); //建立对象box2,指定实参
cout <<"The volume of box2 is"<<box2.volume()<<endl;
return 0;
}
5、使用默认参数的构造函数
现实生活中有的初始值是一定的,因此可以设定默认初始值,再定义对象时不指定实参也可以获得想要的初始值。
#include <iostream>
#include <string>
using namespace std;
class Box
{public:
Box(int h = 10,int w= 10,int len =10);//声明构造成员函数时指定默认参数
int volume(); //声明计算体积的函数
private:
int heigh;
int width;
int length;
};
Box::Box(int h,int w,int len)//在定义构造成员函数时可以不再指定参数的默认值
{
heigh = h;
width = w;
length = len;
}
int Box::volume() //定义计算体积的函数
{
return(heigh*width*length);
}
int main()
{
Box box1; //没有给定实参
cout <<"The volume of box1 is"<<box1.volume()<<endl;
Box box2(15); //只给定一个实参
cout <<"The volume of box2 is"<<box2.volume()<<endl;
Box box3(15,30); //只给定2个实参
cout <<"The volume of box3 is"<<box3.volume()<<endl;
Box box4(15,30,20); //给定3个实参
cout <<"The volume of box4 is"<<box4.volume()<<endl;
return 0;
}

这样做的好处相当于好几个构造函数。
由于容易出错,一般不同时使用构造函数的重载和有默认参数的构造函数。
二、析构函数
析构函数的作用与构造函数的作用相反,它在撤销对象占用的内存之前完成一些清理工作,使这部分内存可以被程序分配给新对象使用,并不是删除对象。在对象的生命周期结束时,会自动执行析构函数。
使用时在类名前加“~”取反运算符。
没有参数,不可以被重载,一个类可以有多个构造函数,但是只能有一个析构函数。
#include <iostream>
#include <string>
using namespace std;
class Student
{public:
Student(int n,string name,char s) //定义构造函数
{
num = n;
name = name;
sex = s;
cout<<"Constructor called."<<endl;
}
~Student() //定义析构函数
{cout<<"Destructor called."<<endl;}
void display() //定义成员函数
{
cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl;
}
private:
int num;
string name;
char sex;
};
int main()
{
Student stud1(10010,"wang_li",'f'); //建立对象
stud1.display();
Student stud2(10011,"zhang_li",'m'); //建立对象
stud2.display();
return 0;
}

从上面的结果可以看出构造函数与析构函数的关系:
先构造的后析构,后构造的先析构,它相当于一个栈,先进后出
三、对象数组、指针
1、对象数组
由于当对象较多时,如果还一个一个的去定义,那将是一个不明智的做法,因此可以定义一个对象数组,其中每一个元素就是一个对应的对象。
使用方法:
#include <iostream>
#include <string>
using namespace std;
class Box
{public:
Box(int h = 10,int w= 10,int len =10):heigh(h),width(w),length(len) {};
//声明有默认参数的构造函数,用参数初始化表对数据成员赋值
int volume(); //声明计算体积的函数
private:
int heigh;
int width;
int length;
};
int Box::volume() //定义计算体积的函数
{
return(heigh*width*length);
}
int main()
{
Box a[3] = { //定义对象数组
Box(10,12,15), //调用构造函数Box,提供第一个元素的实参
Box(15,18,20), //调用构造函数Box,提供第二个元素的实参
Box(16,20,25) //调用构造函数Box,提供第三个元素的实参
};
cout <<"The volume of a[0] is"<<a[0].volume()<<endl;
cout <<"The volume of a[1] is"<<a[1].volume()<<endl;
cout <<"The volume of a[2] is"<<a[2].volume()<<endl;
return 0;
}

2、对象指针
建立对象的时候看,系统会给对象分配一定的存储空间,以存放其成员,对象空间的起始地址就是对象的指针。
(1)定义指向对象的指针变量
形式:*类名 对象指针名
(2)定义指向对象数据成员的指针变量
形式:*数据类型名 指针变量名
(3)定义指向公共成员函数的指针变量
形式:*数据类型名(类名::指针变量名)(参数列表)
使指针变量指向一个公共成员函数的一般形式:
指针变量名 = &类名::成员函数名;
对象指针的用法:
#include <iostream>
#include <string>
using namespace std;
class Time
{public:
Time(int,int,int); //声明有参数的构造函数
int hour;
int minute;
int sec;
void get_time(); //声明公有成员函数
};
Time::Time(int h,int m,int s)//定义构造函数
{
hour = h;
minute = m;
sec = s;
}
void Time::get_time() //定义共有成员函数
{
cout<<hour<<" "<<minute<<" "<<sec<<endl;
}
int main()
{ Time t1(10,20,30); //定义Time对象t1
int *p1 = &t1.hour; //定义指向整型数据的指针变量p1,并使p1指向t1.hour
cout<<*p1<<endl; //输出p1所指的数据成员t1.hour
t1.get_time(); //调用对象t1的成员函数get_time
Time *p2 = &t1; //定义指向Time类对象的指针变量p2,并使p2指向t1
p2->get_time(); //调用p2所指向对象(t1)的get_time函数
void (Time :: *p3)();//定义指向Time类共有成员函数的指针变量p3
p3 = &Time::get_time;//使p3指向Time类共有成员函数get_time
(t1.*p3)(); //调用对象t1中p3所指向的成员函数(即t1.get_time)
return 0;
}

四、常对象
为了解决既要使数据能在一定范围内共享,又要保证它不被任意修改,这时可以使用const,即把有关的数据定义为常量。
定义常对象的一般形式:
类名 const 对象名[(实参列表)];或者
const 类名 对象名[(实参列表)];
如果一个对象被声明为常对象,则不能调用该对象的非const型成员函数(除了系统自动调用的隐式构造函数和析构函数),这样做的作用是防止这些函数修改对象中数据成员的值。
1、常对象成员
(1)常数据成员
用关键字const来声明常数据成员,只能通过构造函数的参数初始化表对常数据成员进行初始化。
(2)常成员函数
如果将成员函数声明为常成员函数,则只能引用本类中的数据成员,而不能修改它们。
2、常对象与常指针
(1)指向对象的常指针
将指向对象的指针变量声明为const型,并使之初始化,这样指针始终保持为其初值,不能改变,即其指向始终不变。
一般形式:
类名 * const 指针变量名 = 对象地址;
如:
Time t1(10,12,15),t2;//定义对象
Time * const ptrl &t1;//规定指针ptrl的值是常量,并指向对象t1,此后不能再改变
ptrl = &t2;//错误,ptrl不能改变指向
(2)指向常对象的指针变量
定义指向常变量的指针变量的一般形式:
const 类型名 *指针变量名;
例如:
const char *ptr;
表示指针变量ptr指向的char变量是常变量。
a.如果一个变量已被声明为常变量,只能用指向常变量的指针变量指向它,而不能用一般的指针变量去指向它。
const char c[] = "boy";//定义const型的char数组
const char *p1; //定义p1指向const型的char变量的指针变量
p1 = c; //合法
char *p2 = c;//不合法
b.指向常变量的指针变量除了可以指向常变量外,还可以指向未被声明为const的变量,此时不能通过指针变量改变所指向变量的值。
char c1 = 'a'; //定义字符变量c1,并未声明为const
const char *p; //定义一个指向常变量的指针变量p
p = &c1; //使p指向字符变量c1
*p = 'b'; //不合法,
c1 = 'b';//合法,没有通过p访问c1,c1不是常变量
(3)指向常对象的指针变量
前面介绍的是指向常变量的情况,常对象的情况也是一样的。
a.如果一个对象被声明为常对象,只能用指向常对象的指针变量指向它,不能用一般的指针。
b.如果定义一个指向常对象的指针变量,并使它指向一个非const的对象,则其指向的对象是不能通过指针来改变的。
指向常对象的指针变量与指向对象的常指针变量在形式上的区别
Time * const p;//指向对象的常指针变量
const Time *p;//指向常对象的指针变量
c.如果定义了一个指向常对象的指针变量,是不能通过它改变所指向的对象的值的,但是指针变量本身的值是可以改变的。
const Time *p = &t1;//定义指向常对象的指针变量p,并指向对象t1
p =&t2;//p改为指向t2,合法
五、对象的常引用
一个变量的引用就是变量的别名。
void fun(Time &t)//此处是引用符,形参t是Time类对象的引用
{t.hour = 18;}
int main()
{Time t1(10,12,18);//t1是Time类对象
fun(t1);//实参是Time类对象,可以通过引用来修改实参t1的值
}
如果不希望在函数中修改实参t1的值,可以把引用变量t声明为ocnst(常引用),函数原型为:
void fun(const Time &t);
则在函数中不能改变t的值,也就是不能改变其对应的实参t1的值。
C++中,经常使用常指针和常引用做函数参数。这样既能保证数据安全,还可以提高程序运行效率。
六、对象的动态建立和释放
用new运算符动态地分配内存后,将返回一个指向新对象的指针的值,即所分配的内存空间的起始地址。
Box *pt;//定义一个指向Box类对象的指针变量pt
pt = new Box;//在pt中存放了 新建对象的起始地址
在程序中就可以通过pt访问这个新建的对象。如
cout<<pt->height;//输出该对象的height成员
cout<<pt->volume();//调用该对象的volume函数,计算并输出体积
C++还允许在执行new时,对新建立的对象进行初始化
Box *pt = new Box(12,15,18);
调用对象即可以通过对象名,要可以通过指针,它主要应用于动态的数据结构,如链表。
在不再需要使用由new建立的对象时,可以用delete运算符给予释放,如:
delete pt;//释放pt指向的内存空间
在执行delete运算符时,在释放内存空间之前,自动调用析构函数,完成有关善后清理工作。
七、对象的赋值和复制
1、对象的赋值
这里指的对象的值是指对象中所有数据成员的值,实际上是通过成员复制来完成的。
对象赋值的一般形式为:
对象名1 = 对象名2;
对象的赋值只对其中的数据成员赋值,而不对成员函数赋值。
2、对象的复制
在建立一个新对象时调用一个特殊的构造函数-----复制构造函数
一般形式:
类名 对象2(对象1);
Box box2(box1);
用对象1复制了对象2
C++还提供了另一个方便的复制形式:
类名 对象名1 = 对象名2;
Box box2= box1,box3 = box2;
其作用都是调用复制构造函数
八、静态成员
如果想在同类的多个对象之间实现数据共享,也不要用全局对象,可以用静态的数据成员。
1、静态数据成员
在一个类中可以有一个或多个静态数据成员,所有的对象共享这些静态数据成员,都可以引用它。
定义时在最前面加上static关键词
静态数据成员只能在类体外进行初始化。一般形式为:
数据类型 类名::静态数据成员名 = 初值;
注意:不能用参数初始化表对静态数据成员初始化
#include <iostream>
#include <string>
using namespace std;
class Box
{public:
Box(int,int); //声明构造函数
int volume(); //声明计算体积的函数
static int height; //把height定义为公用的静态的数据成员
int width;
int length;
};
Box::Box(int w,int len) // 通过构造函数对width和length赋初值
{
width = w;
length = len;
}
int Box::volume() //定义计算体积的函数
{
return(height*width*length);
}
int Box::height = 10; //对静态数据成员height初始化
int main()
{
Box a(12,15),b(20,25);
cout <<a.height <<endl; //通过对象名a引用静态数据成员
cout <<b.height<<endl; //通过对象名b引用静态数据成员
cout <<Box::height<<endl; //通过类名引用静态数据成员
cout <<a.volume()<<endl;//调用volume函数,计算体积,输出结果
}

有了静态数据成员,各对象之间的数据有了沟通的渠道,实现数据共享,可以不使用全局变量,全局变量破坏了封装的原则。
2、静态成员函数
在类中声明函数的前面加static就变成了静态成员函数。
在类外调用公用的静态数据成员函数,要用类名和域运算符“::”。如 Box :: volume();
与静态数据成员不同,静态成员函数的作用不是为了对象之间的沟通,而是为了能处理静态数据成员。
静态成员函数可以直接引用本类中的静态数据成员。
C++程序中养成的好习惯:只用静态成员函数引用静态数据成员,而不引用非静态数据成员。
九、友元
在C++中,一种关系以关键字friend声明,中文译为友元,友元可以访问与其有好友关系的类中的私有成员。友元包括友元函数和友元类。
1、友元函数
如果在本类以外的其他地方定义了一个函数(可以是不属于任何类的非成员函数,也可以是其他类的成员函数),在类体中用friend对该函数进行声明,此函数就称为本类的友元函数。
(1)将普通函数声明为友元函数
#include <iostream>
#include <string>
using namespace std;
class Time
{public:
Time(int,int,int); //声明有参数的构造函数
friend void display(Time &);//声明display函数为Time类的友元函数
private:
int hour;
int minute;
int sec;
};
Time::Time(int h,int m,int s)//定义构造函数,给hour,minute,sec赋初值
{
hour = h;
minute = m;
sec = s;
}
void display(Time &t) //这是友元函数,形参t是Time类对象的引用
{
cout<<t.hour<<":"<<t.minute<<":"<<t.sec<<endl; //由于display不是Time类的成员函数,不能默认引用Time类的数据成员,必须指定要访问的对象
}
int main()
{ Time t1(10,20,30); //定义Time对象t1
display(t1); //调用display函数,实参t1是Time类对象
return 0;
}

友元函数引用这些私有函数一定要加上对象名。
(2)友元成员函数
friend函数也可以是另一个类中的成员函数
#include <iostream>
#include <string>
using namespace std;
class Date; //对Date类的提前引用声明
class Time
{public:
Time(int,int,int); //声明有参数的构造函数
void display(Date &);//display是成员函数,形参是Date类对象的引用
private:
int hour;
int minute;
int sec;
};
class Date
{
public:
Date(int,int,int);
friend void Time::display(Date &);//声明Time类中的display函数为本类的友元成员函数
private:
int month;
int day;
int year;
};
Time::Time(int h,int m,int s)//定义构造函数,给hour,minute,sec赋初值
{
hour = h;
minute = m;
sec = s;
}
void Time::display(Date &d) //display函数的作用是输出年,月,日和时分秒
{
cout<<d.month<<"/"<<d.day<<"/"<<d.year<<endl; //引用Date类对象中的私有数据
cout<<hour<<":"<<minute<<":"<<sec<<endl; //引用本类对象中的私有数据
}
Date::Date(int m,int d,int y)//定义类Date的构造函数
{
month = m;
day = d;
year = y;
}
int main()
{ Time t1(10,20,30); //定义Time对象t1
Date d1(3,9,2021); //定义Date对象t2
t1.display(d1); //调用t1中的display函数,实参是Date类对象d1
return 0;
}
一个函数(包括普通函数和成员函数)可以被多个类声明为“朋友”,这样就可以引用多个类中的私有数据。
2、友元类
也可以将一个类声明为另一个类的“朋友”。
声明友元类的一般形式为:
friend 类名;
在A类的定义体中用以下语句声明B类为其友元类。
friend B;
注意:
(1)友元的关系是单向的而不是双向的;如果声明了B类是A类的友元类,不等于A类是B类的友元类
(2)友元的关系不能传递;
友元有助于数据共享,提高程序效率,但也会对封装性和信息隐蔽有一定的副作用,因此应该在数据共享与信息隐蔽之间选择一个恰当的平衡点。
十、类模板
类似于函数模板,当有两个或者多个类,其功能是相同的,仅仅是数据类型不同,C++提供了模板(template)的功能。
(1)声明类模板时要增加一行
template <class 虚拟类型参数名>
类型参数名是一个虚拟的类型名,自己命名,在以后将被一个实际的类型名取代。
(2)用类模板定义对象时用以下形式:
类模板名 <实际类型名> 对象名;
类模板名 <实际类型名> 对象名(实参列表);
(3)如果在类模板外定义成员函数,应写成:
template <class 虚拟类型参数名>
函数类型 类模板名 <虚拟类型参数> ::成员函数名(函数形参列表){};
#include <iostream>
#include <string>
using namespace std;
template <class numtype> //定义类模板
class Compare
{public:
Compare(numtype a,numtype b)
{x= a;y = b;}
numtype max()
{return (x > y)? x:y;}
numtype min()
{return (x < y)? x:y;}
private:
numtype x,y;
};
int main()
{ Compare <int> cmp1(3,7);
cout <<cmp1.max()<<"is the Maxomum of two integer numbers."<<endl;
cout <<cmp1.min()<<"is the Minimum of two integer numbers."<<endl<<endl;
Compare <float> cmp2(23.21,70.51);
cout <<cmp2.max()<<"is the Maxomum of two float numbers."<<endl;
cout <<cmp2.min()<<"is the Minimum of two float numbers."<<endl<<endl;
Compare <char> cmp3('a','A'); //定义Time对象t1
cout <<cmp3.max()<<"is the Maxomum of two characters."<<endl;
cout <<cmp3.min()<<"is the Minimum of two characters."<<endl<<endl;
return 0;
}

类模板的类型参数可以有一个或者多个,每个类型前面都必须加class
template <class T1, class T2>