C++入门之002:类
1、引言:类方法实现求取整型数组中的元素的最大值
#include<iostream>
using namespace std;
class Array_max //声明类
{
public: //以下3行为成员函数原型声明
void set_value(); //设置原始数组的值
void max_value(); //找出数组中的最大元素
void min_value(); //找出数组中的最小值
void show_value(); //输出最大值
private:
int array[10]; //整型数组
int max; //max用来存放最大值
int min; //min用来存放最小值
};
void Array_max::set_value() //成员函数定义,向数组元素输入数值
{
int i;
for(i=0;i<10;i++)
cin>>array[i];
}
void Array_max::max_value() //成员函数定义,找数组元素中的最大值
{
int i;
max=array[0];
for(i=1;i<10;i++)
if(array[i]>max) max=array[i];
}
void Array_max::min_value()
{
int j;
min=array[0];
for(j=1;j<10;j++)
if(array[j]<min) min=array[j];
}
void Array_max::show_value() //成员函数定义,输出最大值
{
cout<<"max="<<max;
cout<<"min="<<min<<endl;
}
int main()
{
Array_max arrmax; //定义对象
arrmax.set_value(); //调用arrmax的set_value函数,向数组元素输入数值
arrmax.max_value(); //调用arrmax的max_value函数,找出数组元素中的最大值
arrmax.min_value(); //调用arramax的min_value函数,找出数组元素中的最小值
arrmax.show_value(); //调用arrmax的show_value函数,输出数组元素中的最大值
return 0;
}
二、注意事项
① 在类外定义函数时,应指明函数的作用域(如void Time::set_time())。在成员函数引用本对象的数据成员时,只需直接写数据成员,C++系统会把它默认为本对象的数据成员。也可以显式地写出类名并使用域运算符。
② 应注意区分什么场合用域运算符" :: ",什么场合用成员运算符“.”,类型是抽象的,对象是具体的。定义成员函数时应该指定类名,因为定义的是该类中的成员函数,而调用成员函数时应该指定具体的对象名。后面不是跟域运算符 “ :: ”,而是跟成员运算符“.”。
③ 应注意成员函数定义与调用成员函数的关系,定义成员函数只是设计了一组操作代码,并未实际执行,只有在被调用时才真正地执行这一组操作。
④ 大多数情况下,主函数中甚至不出现控制结构(判断结构和循环结构),而在成员函数中使用控制结构。在面向对象的程序设计中,最关键的工作是类的设计。所有的数据和对数据的操作都体现在类中。可见,类定义好,编写程序的工作就显得简单了。
三、对象初始化
① 对象是一个实体,它反映了客观事物的属性(例如时钟的时、分、秒的值),是应该有确定的值。如果在建立对象时,未进行赋值,则它的值使不可预知的,因为在系统为它分配内存时,保留了这些存储单元的原状,这就成为了这些数据成员的初始值。这种情况显然是与人们的要求不相符的。
② 类的数据成员是不能在声明类时初始化的。下面的写法是错误的:
class Time
{
hour=0;
minute=0;
sec=0;
}; //不能在类定义中对数据成员初始化
因为类并不是一个实体,而是一种抽象类型,并不占存储空间,显然无处容纳数据。
如果一个类中所有的成员都是公用的,则可以在定义对象时对数据成员进行初始化。如
class Time
{ public: //声明为公用成员
hour;
minute;
sec;
};
Time t1={14,56,30}; //将t1初始化为14,56,30
③ 但是,如果数据成员是私有的,或者类中有private或protected的成员,就不能用这种方法初始化。
④同样,如果对一个类定义了多个对象,而且类中的数据成员比较多,那么,程序就显得非常臃肿烦琐,这样的程序哪里还有质量和效率?
⑤构造函数
为了解决③与④的问题,C++提供了构造函数来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。构造函数的名字必须与类名同名,而不能由用户任意命名,以便编译系统能识别它并把它作为构造函数处理。
它不具有任何类型,不返回任何值。构造函数的功能是由用户定义的,用户根据初始化的要求设计函数体和函数参数。
什么时候调用构造函数? 答案:在类对象进入其作用域时调用构造函数。例如,在函数fun1的开头定义了一个对象a,则在执行fun1函数时,就要建立对象a,对象a就有了自己的作用域,或者说,对象a的生命周期开始了。
构造函数没有返回值,因此也不需要在定义构造函数时声明类型,这是它和一般函数的一个重要的不同之处,不能写成
void Time()
{...}
构造函数的作用只是对对象进行初始化。
构造函数不需用户调用,也不能被用户调用,下面用法是错误的:
t1.Time() //企图用调用一般成员函数的方法来调用构造函数
构造函数是在定义对象时由系统自动执行的,而且只执行一次。构造函数一般声明为public。
在构造函数的函数体中不仅可以对数据成员赋初值,而且可以包含其他语句,例如:cout语句。但是一般不提倡在 构造函数中加入与初始化无关的内容,以保持程序的清晰。
如果用户自己没有定义构造函数,则C++系统会自动生成一个构造函数,只是这个构造函数的函数体是空的,也没有参数,不执行初始化操作。
⑥带参数的构造函数
问题引导:构造函数不带参数,在函数体中对数据成员赋初值。这种方式使该类的每一个对象都得到同一组初值。但是有时我们希望对不同的对象赋予不同的初值,这时就无法使用上面的办法来解决了。
解决方法:可以采用带参数的构造函数,在调用不同对象的构造函数时,从外面将不同的数据传递给构造函数,以实现不同的初始化。
构造函数名(类型1 形参1,类型2 形参2,...)
而与之相对应的,定义对象的一般格式为
类名 对象名(实参1,实参2,...)
编程实例2:
1) 带参数的构造函数中的形参,其对应的实参在定义对象时给定;
2)用这种方法可以方便地实现对不同的对象进行不同的初始化。
⑦用参数初始化表对数据成员初始化
这是c++提供的一种机制,除了使用构造函数可以对类中的成员变量进行初始化,还可以使用参数初始化列表。这种方法不在函数体内对数据成员初始化,而是在函数首部实现。这样可以减少函数体的长度。
举例1如下:
具体格式如下:
class BOX
{
public:
BOX(int h,int w ,int len);
private:
int height;
int width;
int length;
};
使用参数初始化表初始化的方式:
BOX::BOX(int h ,int w ,int len):height(h), width(w), length(len){}
传统的方式是这样子写的:
BOX::BOX(int h ,int w ,int len)
{
height= h;
width= w;
length= len;
}
注意,使用参数列表初始化是不能初始化静态变量的:比如说下面的
class BOX
{
public:
BOX(int h,int w ,int len);
private:
static int height;
int width;
int length;
};
因为静态成员与对象无关,属于整个类,构造函数是构造某个具体的对象
举例2如下
/*定义Student.h头文件*/
#include<string>
using namespace std;
class Student
{
public:
void display();
Student(int a,string b,char c);
private:
int num;
string name;
char sex;
};
/*Student.cpp主文件*/
#include<iostream>
#include<string>
#include"Sudent.h"
void Student::display()
{
cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl;
}
Student::Student(int a,string b,char c):num(a),name(b),sex(c){} //参数初始化表对数据成员初始化
/*在函数首部的末尾加一个冒号,然后列出参数的初始化表。初始化表表示:用形参a的值初始化数据成员num,用形参b的值初始化数据成员name,用形参c的值初始化数据成员sex*/
⑧构造函数的重载
构造函数重载: 构造函数具有相同的名字,而参数个数或参数类型不相同。
⑨析构函数
析构函数的作用并不是删除对象,而是在撤销对象占用的内存之前完成一系列的操作,使这部分内存可以被程序分配给新对象使用。
析构函数不返回任何值,没有函数类型,也没有函数参数。由于没有函数参数,因此它不能被重载。一个类可以有多个构造函数,但是只能有一个析构函数。
//实例
#include<string>
#include<iostream>
using namespace std;
class Student //声明Student类
{
public:
Student(int n, string nam,char s) //定义构造函数
{
num=n;
name=nam;
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_fun",'m');
stud2.display();
return 0;
}
备注: 调用构造函数和析构函数的顺序
先构造的后析构,后构造的先析构。它相当于一个占,先进后出。
五、对象指针
在建立对象时,编译系统会为每一个对象分配一定的存储空间,以存放其成员。
对象空间的起始地址就是对象的指针。
可以定义一个指针变量,用来存放对象的指针。
指向对象成员的指针又可分为指向对象数据成员的指针与指向对象成员函数的指针。
实例:
六、this指针
①问题?如果对同一个类定义了n个对象,则有n组同样大小的空间以存放n个对象中的数据成员。但是,不同的对象都调用同一个函数代码。那么,当不同对象的成员函数引用数据成员时,怎么能保证引用的是所指定的对象的数据成员呢?
② 答:其实在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this,他是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。
this指针是隐式使用的,它是作为参数被传递给成员函数的。
③释义
常规显示:
int Box::volume()
{return (height*width*length);}
而C++把它处理为:
int Box::volume(Box *this)
{
return(this->height*this->width*this->length);
}
七、共享数据的保护
在C++面向对象程序设计中,经常用常指针和常引用作为函数参数。这样既能保证数据安全,是数据不能被随意修改,在调用函数时又不必建立实参的拷贝,可以提高程序运行效率。
形式 | 含义 |
---|---|
Time const t1; | t1是常对象,其值在任何情况下都不能改变 |
void Time::fun() const | fun是Time类中的常成员函数,可以引用,但不能修改本类中的数据成员 |
Time *const p; | p是指向Time对象的常指针,p的值(即p的指向)不能改变 |
const Time *p; | p是指向Time类常对象的指针,其指向的类对象的值不能通过指针来改变 |
Time &t1=t; | t1是Time类对象t的引用,二者指向同一段内存空间 |
① 对象的赋值
对象名1=对象名2;
程序片段
int main()
{
Box box1(15,30,25),box2; //定义两个对象box1和box2
box2=box1; //将box1的值赋给box2
}
说明:1)对象的赋值只对其中的数据成员赋值,而不对成员函数赋值。数据成员是占存储空间的,不同对象的数据成员占有不同的存储空间,赋值的过程是将一个对象的数据成员在存储空间的状态复制给另一对象的数据成员的存储空间。而不同对象的成员函数时同一函数代码段,不需要、也无法对它们赋值。
2)类的数据成员中不能包括动态分配的数据,否则在赋值时可能出现严重后果。
②对象的复制
有时需要用到多个完全相同的对象。用初始化的方法比较麻烦。此外,有时需要将对象在某一瞬间的状态保留下来,这就引出了对象的复制机制。
复制方法一:
类名 对象2(对象1);用对象1复制出对象2
C++提供的方便用户的复制形式,用赋值号代替括号:
复制方法二:
类名 对象名1=对象名2
③ 普通构造函数和复制构造函数的区别
1) 在形式上
类名(形参表列) //普通构造函数的声明,如Box(int h,int w, int len)
类名(类名 & 对象名) //复制构造函数的声明,如Box(Box &b);
2)在建立对象时,实参类型不同。系统会根据实参的类型决定调用普通构造函数或复制构造函数。如
Box box1(12,15,16); //实参为整数,调用普通构造函数
Box box2(box1); //实参是对象名,调用复制构造函数
3)调用时机不同
普通构造函数在程序中建立对象时被调用。
复制构造函数在用已有对象复制一个新对象时被调用:
① 程序中需要新建立一个对象,并用另一个同类的对象对它初始化。
②当函数的参数为类的对象时。在调用函数时需要将实参对象完整地传递给形参,也就是需要建立一个实参的拷贝,即按实参复制一个形参,系统是通过调用复制构造函数来实现,这样能保证形参具有和实参完全相同的值。
如: void fun(Box b) //形参是类的对象
{ }
int main()
{
Box box1(12,15,18);
fun(box1); //实参是类的对象,调用函数时将赋值一个新对象b
return 0;
}
③ 函数的返回值是类的对象。在函数调用完毕将返回值带回函数调用处时。此时需要将函数中的对象复制一个临时对象并传给该函数的调用处。
Box f() //函数f的类型为Box类型
{
Box box1(12,15,18);
return box1; //返回值是Box类的对象
}
int main()
{
Box box2;
box2=f(); //调用f函数,返回Box类的临时对象,并将它赋值给box2
}
由于box1是在函数f中定义的,在调用f函数结束时,box1的生命周期就结束了,因此并不是将box1带回main函数,而是在函数f结束前执行return语句时,调用Box类中的复制构造函数,按box1复制一个新的对象,然后将它赋值给box2.
以上几种调用复制构造函数都是由编译系统自动实现的,不必由用户自己去调用。