前期教程
一、概述
建议先看完上面那篇博客。这篇博客主要讲述C++中类和对象的定义与使用。
二、面向对象的基本概念
先介绍一些概念,建立理论印象。
1. 对象与类
所谓对象,其实就是用来描述客观事物的一个实体,比如一辆汽车就是一个对象。描述一个对象时,一般会从它的属性和行为两方面来展开,其中,属性可以认为是对象的静态特征,一些数据;而行为可以认为是对象的动态特征,对应的操作等。
而类,是具有相同属性和服务的一组对象的集合,它是属于该类的全部对象的一个抽象描述,因此同样具有属性和行为两部分,但这里更多的是抽象的概念。
类和对象的关系犹如模具和铸件之间的关系,一个属于某类的对象称为该类的一个实例。用类定义一个对象的过程也叫实例化。
对象的类型就是类。
2. 封装
这里说的封装可不是电路板中元器件的封装,而是指代码的封装。
具体来说就是把对象的属性和服务结合成一个独立的系统单元,比如一个类。同时尽可能隐蔽对象的内部细节,对外形成一个边界,只保留有限的对外接口使之与外部发生联系。像这种只暴露公有接口,而隐藏私有实现的数据称为抽象数据类型。
在类中,常用public关键字来显现类的接口;而用private来隐藏类的实现。通常数据成员是隐藏的,并对不隐藏的成员函数提供支持。
3. 继承
所谓继承,是指特殊类(子类)拥有其一般类(父类)的全部属性和服务,称作特殊类对一般类的继承。比如交通工具是父类,那么地铁就是其中一个子类。当然这个继承可以有多层。
4. 多态性
多态是指在一般类(父类)中定义的属性或行为,被特殊类(子类)继承之后,可以具有不同的数据类型或表现出不同的行为。这使得同一个属性或行为在一般类及其各个特殊类中具有不同的语义。
这个概念初看可能会有点懵,举个例子:动物(父类)中有一个成员函数是发出叫声,猫(子类)中也有一个成员函数也是发出叫声,但它们的返回值不同(一个喵喵喵,一个不确定),那么在调用这个函数时,如果选定的作用域不同,那么得到的返回值也不同。
三、类与对象的特性及其使用
1. 类的声明和对象的定义
从前面讲述的类和对象的关系可以看出,在代码里面,我们都要先声明一个类,然后用这个类来定义对象。值得注意的是:类是抽象的,不占用内存,而对象是具体的,要占用内存。
看一个声明类的示例:
class Student //声明类类名
{
private: //声明私有成员
int num;
char name[20];
char sex;
public: //声明公有成员
void display()
{
cout<<"num:"<<num<<endl;
cout<<"name:"<<name<<endl;
cout<<"sex:"<<sex<<endl;
}
};
从这个示例中,我们可以看出:
- 类内的变量和函数要指明其访问权限,是public还是private,如果不指定默认是private;其中public能在类外访问,一般用来修饰一些需要调用的函数,private不能在类外访问,一般用来修饰为public提供支持的变量或函数。
- 除了public和private之外,还有protected,它与private类似,差别在于protected可以被派生类的成员函数访问。
- 类中的函数可以直接在类内定义,但是这样定义得到的函数都是内置函数,所以只适用于短小的程序。
- 声明完类之后,要在最后加上分号!!! 这个规则和结构体类似。
声明完类之后,接下来就是使用类来定义对象了,一般来说,定义对象有三种方式:
- 声明完类之后再定义对象。
class Student stu1, stu2; //这种是从C语言中继承下来的
Student stu3, stu4; //C++独有的方式而且使用也更广泛
- 声明类的同时定义对象。
class CDate
{
int year;
int month;
int day;
public:
void SetDate(int, int, int);
void ShowDate();
}myBirthday; //同时创建对象myBirthday
- 不出现类名,直接定义对象。
class //class开头 无类名
{
private: //声明私有部分
…
…
public: //声明公用部分
…
…
}stu1,stu2;//定义了两个Student类的对象stu1与stu2
这种方式也是合法的,但是不建议使用。 参考链接及代码来源
此外,也可以使用类来定义对象数组和对象指针。需要注意指针访问成员函数和成员变量时应该用->
符号,且在使用时一定要初始化。
Student *stu1, st[10];
stu1 = &st[0]; //指针使用前一定要初始化,即指向一个实际的对象。
//调用时
stu1->num = 110;
//或者这样
(*stu1).num = 110;
st[1].num = 111;
2. 对象的动态建立与释放
在前面,我们学习了new
和delete
两个运算符来动态分配和删除变量的内存,其实它们不仅可以用在变量,还可以用在对象上。
使用示例:
class Box
{
....
};
int main()
{
Box *pt, box1;
pt = new Box;
pt = &box1; //定义一个指针指向box1这个对象。
....
delete pt; //释放内存
}
3. 类的成员函数
前面提到,类中的成员函数也可以在类内直接定义,但大部分的程序不会这样干,因为它会自动变成内置函数,不适合较大的程序。一般来说,都是先声明类,同时声明类中的成员函数,然后再类外对函数进行定义。类外函数定义的基本结构:
- this指针
在定义类中的成员函数时,有时会需要访问类内的成员变量或其他函数,为了指明所访问的变量是类内变量,有时会采用this
来指代本类。举个例子
class Box
{
int length;
int width;
int height;
public:
void getvolume();
}
void Box::getvolume()
{
cout << length*width*height << endl;
}
//也可以写成下面这样
void Box::getvolume()
{
cout << (this->length)*(this->width)*(this->height) << endl;
}
所以,this
更多地可以看成是类的别名。
4. 类的封装
如果一个类仅被一个程序调用,可将类的声明和成员的定义放在文件开头。但如果类被多个程序调用,则可以将类的声明(包括成员函数的声明)放在一个头文件中(.h),用户使用类时,只需要include该头文件即可。这就是类的封装。而这也是形成类库的方式。
// student.h(头文件)
class Student
{
...
}
// Student.cpp (实现文件,必须与头文件同名)
void Student::func()
{
...
}
....
// main.cpp (主程序)
#include "Student.h" //包含头文件,一般自定义的头文件要使用双引号。
int main()
{
...
}
此外,如果为了隐藏源码实现,但又能够让用户使用该库中的类,就可以将.cpp和.h文件单独编译成一个.obj文件,然后由用户添加到工程项目中,这样也能实现内部类的调用。
5. 构造函数
在声明类时,往往会将一些数据成员设为private,但这样也就意味着外界无法访问这些变量,也就无法赋值,因此往往还需要添加一个设置数据的函数,有点不太方便,而构造函数就是定义类时自带的一个可以用来初始化类内数据变量的一个函数。【类内的成员变量不能在声明时直接初始化一个值,除非用const static
修饰 参考链接】
-
5.1 构造函数的特性
- 构造函数是专门用来处理函数对象初始化的。
- 构造函数是一种特殊的成员函数。
- 构造函数的名字必须与类名相同。
- 构造函数是系统自动调用的,且只执行一次。
- public,无返回值,无需定义返回值类型。
-
5.2 构造函数的形式
-
无参数
每个类都要有构造函数,如果没写,编译器会为这个类调用一个默认构造函数——一个没有形参,也没有内容的一个函数,相当于没有初始化的作用。如果自己再定义一个无参数的构造函数,就会 “覆盖” 掉默认构造函数,那么在初始化时编译器会自动调用自定义的构造函数,而不是默认构造函数。class student { private: int num; int sex; public: student() //自定义一个无参数的构造函数 { num = 10; sex = 5; } void getnum(); }; void student::getnum() { cout<<num*sex<<endl; } int main() { student stu1; //定义变量时就已经调用自定义构造函数进行初始化了 stu1.getnum(); system("pause"); return 0; }
-
带参数
如果自定义一个带参数的构造函数,也会“覆盖”默认构造函数,如果初始化时什么都不加,编译器会报错:“没有找到合适的构造函数”。
此外,定义多个参数不同的构造函数也是允许的,也就是允许构造函数重载,这样就可以根据传递的参数不同来进行不一样的初始化。参考下面这个例子。
class student { private: int num; int sex; public: student(int num1) { num = num1; sex = num * 2; } student(int num1, int sex1) { num = num1; sex = sex1; } void getnum(); }; void student::getnum() { cout<<num*sex<<endl; } int main() { //student stu1; //err: 没有合适的构造函数 student stu2(10); //调用第一个构造函数 student stu3(10, 12); //调用第二个构造函数 stu2.getnum(); stu3.getnum(); system("pause"); return 0; }
- 带缺省参数
构造函数也是支持缺省参数函数的,也就是对一些参数指定默认值,但需要注意的是,带缺省参数的函数如果重载的话,一定不能冲突,这个在上一篇教程中已经强调过。
需要注意:如果定义了缺省参数的构造函数,相当于对构造函数进行重载;而且指定默认值要在声明时指定。
-
-
5.3 参数初始化列表
C++类中成员变量的初始化有两种方式:构造函数初始化列表和构造函数体内赋值。下面看看两种方式有何不同。class Animal { public: Animal(int weight,int height): //A初始化列表 m_weight(weight), m_height(height) { } Animal(int weight,int height) //B函数体内初始化 { m_weight = weight; m_height = height; } private: int m_weight; int m_height; };
此外,类内不同类型的变量初始化的方式也不太一样:
- 一般变量(int a) 可以在初始化列表里或者构造函数里初始化,不能直接初始化或者类外初始化;
- 静态成员变量(static int a) 必须在类外初始化;
- 常量(const int a) 必须在初始化列表里初始化;
- 静态常量(static const int a) 必须只能在定义的时候初始化(定义时直接初始化)。
可以通过以下代码示例更直观的感受:#include <iostream> #include <string> using namespace std; class Test { private: int a; static int b; const int c; static const int d=4; public: Test():c(3) //a(1)或者在初始化列表里初始化 { a=1; } }; int Test::b=2; void main() { Test t; }
6. 析构函数
- 析构函数主要用来完成对象被删除之前的一些清理工作,来释放空间。
- 析构函数没有参数,也没有返回值类型说明。
- 析构函数一般在对象的生存期结束的时刻,系统自动调用它,然后再释放此对象所属的空间。
- 一个类只能有一个析构函数,不能重载。
- 如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数。
值得一提的是,所谓生存期结束,是指执行return 0;
这行代码时才会显示,所以调试的时候需要注意。另外,如果一个程序有多个对象,那么一般是先构造的先析构。
#include <iostream>
#include <string>
using namespace std;
class Student
{
public:
Student(int n,string nam,char s)
{
num=n;
name=nam;
sex=s;
cout<<"Construtor called"<<endl;
}
~Student()
{
cout<<"Destructorcalled"<<num<<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,"wng",'f');
stud1.display();
Student stud2(10011,"zng",'m');
stud2.display();
return 0;
}
这个程序在Devc++中执行得到的结构如下图所示:
注意:析构函数不仅仅是在对象生命期结束才会调用,还有其他的一些情况,参考这个链接。如果想加深对这方面的认识,可以看看这篇文章里面的题目。