C++核心编程——类与对象基础
- 类与对象
- 封装
- 构造函数
- 普通构造
- 拷贝构造
- 初始化成员列表(补充)
- 析构函数
- 对象数组
- 对象指针
- 指向对象的指针
- 指向对象成员的指针
- this指针
- 静态成员
- 静态数据成员
- 静态成员函数
- 友元
- 普通函数做友元函数
- 友元成员函数
- 友元类
类与对象
C++面向对象的三大特性为:继承、封装、多态
C++认为万事万物都皆为对象,任何一个对象都应当具有两个要素,即属性和方法
例如:
- 人可以作为对象,属性有姓名、年龄、身高、体重…,行为有走、跑、跳、吃饭、唱歌…
- 车也可以作为对象,属性有轮胎、方向盘、车灯…, 行为有载人、放音乐、放空调…
具有相同性质的对象,我们可以抽象称为类,人属于人类,车属于车类
封装
封装的意义
- 将有关的数据和操作代码封装在一个对象中,形成一个基本单位,各个对象之间相互独立,互不干扰
- 将对象中的某些部分对外隐蔽,即隐蔽其内部细节,只留下少量接口,以便与外部连接,接收外部消息
类的声明与定义:
class Student
{
public:
//定义一个成员函数
void display()
{
cout <<"num:" << num << endl;
cout <<"name:" << name << endl;
cout <<"sex:" << sex << endl;
}
private:
// 声明三个成员变量
int num;
char name[20];
char sex;
};
可以看到,声明类的方法是由声明结构体类型的、的方法发展而来的,一般把数据隐藏起来,而把成员函数作为对外界的接口。
class 类名
{
private:
属性 / 行为
public:
属性 / 行为
};
除private和public外,还有protected权限,class类的默认权限为private其中三种权限数据的访问范围如下表:
权限 | 数据访问空间 |
---|---|
private | 类内可以访问 类外不可以访问,可被派生类访问(与protected 的区别) |
public | 类内可以访问 类外可以访问 |
protected | 类内可以访问 类外不可以访问 |
实例化对象
实例化2个Student 类的对象stu1与stu2
Student stu1,stu2;
成员对象的引用
对象名 . 成员名
stu1.num = 100;
stu1.display();
Student *p // 定义指针p
p = &stu1; // p指向对象stu1
cout << p->num << endl;
Student &t = stu1; // 定义Student 类的的引用t,并初始化为stu1
cout << t.num << endl;
成员函数
在类的内部对函数做声明,而在类外定义函数,这是程序设计的一种良好的习惯。这样不仅可以减少类体的长度,使类体清晰,便于阅读,而且能使类的接口和类的实现细节分离。
class Student
{
public:
//定义一个成员函数
void display();
private:
// 声明三个成员变量
int num;
char name[20];
char sex;
};
void Student :: display()
{
cout <<"num:" << num << endl;
cout <<"name:" << name << endl;
cout <<"sex:" << sex << endl;
}
- 成员函数的存储方式:同一类的不同对象中的数据成员的值一般是不相同的,而不同对象的函数的代码是相同的,不论调用哪一个对象的函数的代码,其实调用的都是相同内容的代码。
- 不论成员函数在类内定义还是在类外定义成员函数的代码段的存储方式是相同的,都不占用对象的存储空间。
构造函数
普通构造
问题引入:怎么对成员变量初始化?
以下初始化是错误的:
class Student
{
private:
string name="zhangsan1";
int age=21;
char sex='m';
int score=100;
};
这种初始化方法显然是错误的,因为类并不是一个实体,而是一种抽象类型,并不占存储空间,显然无处容纳数据。
为了解决这个问题,C++提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行,其中函数名称与类名相同。
例如:
Student::Student() //无参构造,默认
{
name = "zhangsan";
age = 20;
sex = 'm';
score = 100;
}
构造函数语法:类名(){}
构造函数特点:
- 函数无返回值也不写void
- 函数名与类名相同
- 在实例化对象的时候自动调用(不需要用户调用,也不能被调用)
- 主要用于对象成员变量初始化
- 构造函数可以有参数,发生函数重载
经验:通常在构造函数的声明时候指定默认参数,而不能只在定义函数时候指定默认参数
典例:已知长方体的长宽高,求长方体体积
#include<iostream>
using namespace std;
class Box
{
public:
Box(int h=10, int w=10, int l=10); //声明构造函数带默认参数
void getVolume();
private:
int height;
int width;
int length;
};
int main()
{
Box box1; // 10 10 10
Box box2(15); // 15 10 10
Box box3(15,20); // 15 20 10
Box box4(15,20,30); // 15 20 30
box1.getVolume();
box2.getVolume();
box3.getVolume();
box4.getVolume();
return 0;
}
//构造函数实现体
Box::Box(int h, int w, int l) //有参构造
{
height = h;
width = w;
length = l;
}
void Box::getVolume()
{
int volume = height*width*length;
cout << "volume=" << volume << endl;
}
拷贝构造
在上述构造中主要涉及的为普通构造,普通构造有分为无参构造和有参构造,在C++中还存在一种拷贝构造,即构造函数传入的参数是一个类的引用,将传入参数类的属性拷贝到当前类。
拷贝构造的定义:
//拷贝构造函数实现
Person::Person(const Person &p) //拷贝构造 记住这种写法就行
{
//将传入的人身上的所有属性,拷贝到我身上
name = p.name;
age = p.age;
sex = p.sex;
cout << "Person的拷贝构造函数调用" << endl;
}
调用:
Person p2(p1);
在这之前,需要对p1对象进行实例化,并通过有参数构造初始化p1的值,其中p1对象的有参构造函数如下:
//有参构造实现
Person::Person(string name, int age, char sex)
{
this->name = name;
this->age = age;
this->sex = sex;
cout << "Person的有参构造函数调用" << endl;
}
不难看出,在这里Person构造函数发生重载,通过
Person p1("zhangsan",20,'m');
先初始化p1,通过有参构造进行赋值,再通过进行拷贝构造,自动调用拷贝构造函数,则完整程序如下所示:
#include <iostream>
#include <string>
using namespace std;
class Person
{
public:
//构造函数发送函数重载
//有参构造
Person(string name, int age, char sex);
//拷贝构造函数
Person(const Person &p); //拷贝构造 记住这种写法就行
void show_info(); //显示信息
private:
string name;
int age;
char sex;
};
//有参构造实现
Person::Person(string name, int age, char sex)
{
this->name = name;
this->age = age;
this->sex = sex;
cout << "Person的有参构造函数调用" << endl;
}
//拷贝构造函数实现
Person::Person(const Person &p) //拷贝构造 记住这种写法就行
{
//将传入的人身上的所有属性,拷贝到我身上
name = p.name;
age = p.age;
sex = p.sex;
cout << "Person的拷贝构造函数调用" << endl;
}
void Person:: show_info()
{
cout << "name=" << name;
cout << "\tage=" << age;
if(sex = 'm')
cout << "\tsex=男" << endl;
else
cout << "\tsex=女" << endl;
}
int main()
{
Person p1("zhangsan",20,'m');
Person p2(p1);
p1.show_info();
p2.show_info();
return 0;
}
程序运行结果:
Person的有参构造函数调用
Person的拷贝构造函数调用
name=zhangsan age=20 sex=男
name=zhangsan age=20 sex=男
整理/补充
1. 构造函数两种分类方式:
- 按参数分为: 有参构造和无参构造
- 按类型分为: 普通构造和拷贝构造
2. 构造函数调用规则
- 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
- 如果用户定义拷贝构造函数,c++不会再提供其他构造函数
初始化成员列表(补充)
在初始化成员属性时候,C++还提供了一种初始化数据的方法——初始化成员列表,其基本语法为:
类名::构造函数名([参数表])[:初始化成员表]
{
[构造函数体]
}
例:
Person(string name_arg, int age_arg, char sex_arg): name(name_arg), age(age_arg), sex(sex_arg){}
说明:如果数据成员是数组, 则应当在构造函数的函数体中用语句对其赋值, 而不能在参数初始化表中对其初始化。
析构函数
析构函数语法:~类名(){}
析构函数的特点:
- 析构函数,没有返回值也不写void
- 函数名称与类名相同,在名称前加上符号 ~
- 析构函数不可以有参数,因此不可以发生重载(与构造函数不同)
- 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
析构函数的作用:
析构函数的作用在于撤销对象占用的内存之前完成一些清理工作。
实际上,析构函数的作用并不仅限于释放资源方面, 它还可以被用来执行"用户希望在最后一次使用对象之后所执行的任何操作", 例如输出有关的信息。
构造与析构的顺序:
先构造的后析构,后构造的先析构
浅拷贝:简单的赋值拷贝操作
深拷贝:在堆区重新申请空间,进行拷贝操作
对象数组
数组不仅可以由简单变量组成(例如整型数组的每一个元素都是整型变量),也可以由类对象组成(对象数组的每一个元素都是同类的对象)。
//假设已经声明了Student类,定义stu数组,有50个元素
Student stu[50];
如何初始化对象数组:
- 如果构造函数只有一个参数,在定义数组的时候可以直接在等号后面的花括号内提供实参。
Student stu[3]={60,70,80};
- 如果构造函数有多个参数,在花括号中分别写出构造函数名并在括号内指定实参。
其中3个参数分别代表姓名、年龄和分数。Student stu[3]={ Student("zhangsan",18,87), //调用第一个元素的构造函数,提供3个实参 Student("lisi",19,76), //调用第二个元素的构造函数,提供3个实参 Student("wangwu",18,72) //调用第三个元素的构造函数,提供3个实参 };
对象指针
指向对象的指针
在建立对象时,编译系统会为每一个对象分配一定的存储空间,以存放其数据成员。对象空间的起始地址就是对象的指针。可以定义一个指针变量,用来存放对象的地址,这就是指向对象的指针变量。
设假定已定义Student类,在此基础上有以下语句:
Student *pt; //定义pt为指向Student类的类对象指针
Student stu1; //实例化对象stu1
pt = &stu1; //将t1的起始地址赋给pt
定义指向类对象的指针变量的一般形式为:
类名 * 对象指针名
可以通过对象指针访问对象和对象的成员:
*pt //pt所指向的对象,即stu1
(*pt).age //pt所指向的对象的age成员,即stu1.age
pt->name; //pt所指向的对象的name成员,即stu1.name
(*pt).showInfo(); //调用pt所指向对象的showInfo函数,即stu1.showInfo()
pt->showInfo(); //调用pt所指向对象的showInfo函数,即stu1.showInfo()
指向对象成员的指针
对象有地址,存放对象的起始地址的指针变量就是指向对象的指针变量。对象成员也有地址,存放对象成员地址的指针变量就是指向对象成员的指针变量。
1. 指向对象数据成员的指针
- 例如:
int *p1 //指向对象成员的指针变量
- 定义
数据类型名 * 指针变量名 - 引用
p1 = &stu1.age; cout << * p1 << endl;
2. 指向对象成员函数的指针
定义指向对象成员函数的指针变量的方法和定义指向普通函数的指针变量方法有所不同。
指针变量的类型必须与赋值号右侧函数的类型像匹配,要求在以下3方面都要匹配:
- 函数参数的类型和个数
- 函数的返回值的类型
- 所属的类
这里重温指向普通函数的指针变量的定义方法::
void (*p)() //p是指向void 型函数的指针变量
p = fun(); //将fun函数的入口地址给p,p指向fun函数
(*p)(); //调用fun函数
而对于一个对象成员函数的指针变量如下所示:
-
定义p2指向Student类中公用成员函数的指针变量
void (Student::*p2)();
定义公用成员函数的指针变量一般形式为:
数据类型名 (类名::* 指针变量名)(参数列表) -
初始化成员函数指针变量:
p2 = Student::showInfo;
一般形式为:
指针变量名 = & 类名 :: 成员函数名
对象指针典例:
int main()
{
Student stu1("zhangsan",22,90);
int *p1 = &stu1.age; //定义成员数据指针
cout << *p1 << endl;
Student *p2 = &stu1; //定义类指针
p2->showInfo();
void (Student::*p3)(); //定义成员函数指针
p3 = &Student::showInfo;
(stu1.*p3)();
return 0;
}
this指针
在每一个成员函数中都包含一个特殊的指针, 这个指针的名字是固定的, 称为 this。它是指向本类对象的指针, 它的值是当前被调用的成员函数所在的对象的起始地址。
this指针是隐式使用的,它是作为参数被传递给成员函数的,但有时也需要显示使用。
- 当形参和成员变量同名时,可用this指针来区分
Student::Student(string name, int age, int score) { this->name = name; this->age = age; this->score = score; }
静态成员
静态数据成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
静态成员分为:
- 静态成员变量
- 所有对象共享同一份数据(数据共享)(类似于全局变量)
- 必须类内声明,类外初始化
典例:用立方体类Box定义两个对象,引用不同对象中的静态数据成员
#include<iostream>
using namespace std;
class Box{
public:
Box(int w, int l);
int getVolume();
static int height; //把 height 定义为公用的静态数据成员
private:
int width;
int length;
};
int Box:: height = 10; //静态成员类外初始化
Box::Box(int w, int l)
{
width = w;
length = l;
}
//计算体积函数体
int Box::getVolume()
{
int volume;
volume = height * width * length;
return volume;
}
int main()
{
Box box1(15,20);
Box box2(20,30);
cout << box1.height << endl; //通过对象box1引用静态数据成员height
cout << box2.height << endl; //通过对象box2引用静态数据成员height
cout << Box::height << endl; //通过类名引用静态数据成员height
//计算体积
cout << "volume=:" << box1.getVolume() << endl;
return 0;
}
说明:
(1)上面的程序中,将height定义为公用的静态数据成员。
(2)可以看到,在类外可以通过对象名访问公用的静态数据成员,也可以通过类名引用静态数据成员。
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量(静态成员函数没有this指针)
- 静态成员函数属于类的一部分,在类外调用公用的静态成员函数,要用类名和与域运算符“::”
实际上也允许通过对象名调用静态成员变量,如:Box::getVolume();
但这不代表函数属于对象a,只是用a的类型而已a.getVolume();
典例:统计学生平均成绩,使用静态成员函数。
#include<iostream>
using namespace std;
class Student{
public:
Student(string, int, int);
void get_total();
static float get_average();
private:
string name;
int age;
int score;
static int sum; //总分
static int count; //人数
};
//静态成员属性类外声明
int Student::sum = 0;
int Student::count = 0; //计数
Student::Student(string name_temp, int age_temp, int score_temp)
{
name = name_temp;
age = age_temp;
score = score_temp;
}
void Student::get_total()
{
sum = sum + score;
count++; //定义已统计的人数
}
float Student::get_average()
{
return (sum/(float)count);
}
int main()
{
Student stu[3]={
Student("zhangsan",18,87),
Student("lisi",19,76),
Student("wangwu",18,72),
};
int i,n;
cout << "please input the number of students:";
cin >> n;
for(i=0;i<n;i++)
stu[i].get_total();
cout << "the average score of" << n << "student is:" << Student::get_average() << endl;
return 0;
}
说明:
(1)在类中定义两个静态数据成员sun和count,这是因由于这两个数据成员的值需要进行累加,并不属于某一个对象元素,而是各个元素共享的,可以看到他们的值在不断变化,始终不释放内存空间。
(2)get_total属于公用的成员函数,公用的成员函数可以访问对象的一般数据成员和静态数据成员。
(3)get_average属于静态成员函数,静态成员函数只能访问静态数据成员。
思考:如何在get_total中访问stu的数据呢?
A:将对象stu的首地址作为get_total的参数。,即get_total的参数为类指针 Student *p
- get_total定义:
void get_total(Student *stu); void Student::get_total(Student *stu) { sum = sum + stu->score; cout << stu->score << endl; count++; //定义已统计的人数 }
- get_total调用:
for(i=0;i<n;i++) stu[i].get_total(&stu[i]);
友元
如果在本类以外的其他地方定义了一个函数(这个函数可以是不属于任何类的非成员函数,也可以是其他类的成员函数), 在类体中用friend对其进行声明此函数就称为本类的友元函数,友元函数可以访问这个类中的私有成员。
友元目的:让一个函数或者类访问另一个类中私有成员,该函数可以为全局函数,也可以为其他类的成员函数
友元语法:friend 函数声明;
普通函数做友元函数
在上述的函数可以是全局的普通函数,例如:显示时间
#include<iostream>
using namespace std;
class Time{
public:
Time(int h, int m, int s):hour(h),min(m),sec(s){};
friend void dispaly(Time *t); //将display作为类的友元函数, 使得display能够访问类的私有成员
private:
int hour;
int min;
int sec;
};
void dispaly(Time *t)
{
cout << t->hour << ":" << t->min << ":" << t->sec << endl;
}
int main()
{
Time t1(10,17,34);
dispaly(&t1);
return 0;
}
程序第7行将display作为类的友元函数, 使得全局函数display能够访问类的私有成员
友元成员函数
friend 函数不仅可以是一般函数(非成员函数), 而且可以是另一个类中的成员函数。
#include<iostream>
using namespace std;
class Date;
class Time{
public:
Time(int h, int m, int s):hour(h),min(m),sec(s){};
void display(Date &);
private:
int hour;
int min;
int sec;
};
class Date{
public:
Date(int y, int m, int d):year(y),month(m),day(d){};
//友元Time类中的成员函数 display,使其访问该类中的私有成员
friend void Time::display(Date &date);
private:
int year;
int month;
int day;
};
void Time::display(Date &date)
{
cout << date.year << "/" << date.month << "/" << date.day << endl;
cout << hour << ":" << min << ":" << sec << endl;
}
int main()
{
Date date(2023,11,9);
Time time(11,5,23);
time.display(date);
return 0;
}
注意在本程序中调用友元函数访问有关类的私有数据方法:
- 在函数名display 的前面要加display所在的对象名(如time);
- display成员函数的实参是 Date 类对象date, 否则就不能访问对象date中的私有数据;
- 在Time::display函数中引用Date类私有数据时必须加上对象名, 如date.month.
程序说明:
- 程序第四行为提前引用Date类,只包含类名,不包含类体,编译时的是Date是一个类名,类体将在稍后定义,没有此声明会报错。
- 注意程序程在定义Time::display函数之前正式定义Date 类的,因为在Time::display 函数体中要用到 Date类的成员month, day, year;如果不事先定义Date类。程序将报错。
友元类
不仅可以将一个函数声明为一个类的“朋友”,而且可以将一个类(例如B类)声明为另一个类(例如A类)的“朋友”。这时B类就是A类的友元类。友元类B中的所有函数都是A类的友元函数, 可以访问A类中的所有成员。
声明友元类类的一般形式为:
friend 类名;
说明:
- 友元的关系是单向不是双向的
- 友元的关系不能传递
- 在实际工作中, 除非确有必要, 一般并不把整个类声明为友元类, 而只将确实有需要的成员函数声明为友元函数, 这样更安全一些。