类和对象5【C++】
一、对象的动态建立和释放
动态内存分配是指在程序运行期间根据实际需要随时申请内存,并在不需要时释放。
new 和 delete
(1)内存空间申请
(1)new 操作符:表示从堆内存中申请一块空间。
(2)返回值:
申请成功:返回所申请到的空间的首地址
申请失败:返回空指针(NULL)
(3)new 的三种形式:
- new 数据类型
- new 数据类型 (初始化值)
- new 数据类型[常量表达式]
Example:
int* a = new int; int* b = new int(3); // 初始化为3 int* c = new int(); // 初始化为0 int* str = new int[10];
(4)利用动态内存分配在堆中创建对象,系统自动调用相应的构造函数来初始化对象。
Date* date1 = new Date; date1->Print(); Date* date2 = new Date(2021,7,1); date2->Print();
(2)内存空间释放
(1)delete 操作符:表示将从堆内存中申请的一块空间返回给堆。
(2)delete 的两种形式:
- delete 指针名
- delete [ ]指针名
int* a0 = new int; float* a1 = new float(1.1); int* a2 = new int[100]; Date* p = new Date(2021, 7, 1); delete a0; delete a1; delete[]a3; delete p;
(3)注意:
a. new 和 delete 需要配套使用:
b. 在使用 delete 来释放一个指针所指的空间时,必须确保这个指针所指的空间使用
new 申请的,并且只能释放这个空间一次。
int i; int* a = &i; delete a; // 错误,空间不是 new 申请的 int* b = new int(3); delete b; // 正确,空间时 new 申请的 delete b; // 错误,只能释放一次
c. 如果在程序中使用了 new 申请空间,就应该在结束程序前释放所以的申请空间。
int* a = new int; *a = 2; a = new int; // 错误,产生了内存泄漏
d. 当一个指针没有指向合法的空间,或者指针所指的内存已经释放以后,最好将指针的
值设为 NULL。
e. 在堆中创建数组时,其元素只能初始化为元素类型的默认值,不能使用初始化表提供
不相同的初值。
如果是类类型的话,系统会调用默认构造函数对其初始化;如果是其他类型的话,默认
情况下不会对其初始化,如果需要初始化,只能在表达式后面加一对圆括号。
int* a0 = new int[5]; Date* date = new Date[5]; int* a1 = new int[5] = {1,2,3,4,5}; int* a2 = new int[5]();
二、类的组合
类的数据成员是类类型,有类对象作为成员称为组合类。
通过构造函数的初始化表为内嵌对象初始化格式为:
类名 :: 构造函数(参数表): 内嵌对象1(参数表1),内嵌对象2(参数表2),...
{
构造函数体
}
组合类构造函数的执行顺序为:
(1)按内嵌对象的声明顺序依次调用内嵌对象的构造函数;
(2)然后执行组合类本身的构造函数。
Example:
【例题】一个学生类。数据成员包括学生的姓名、学号、性别、出生日期。
【分析】学生的姓名、性别可以用简单的数据类型表示,学生的学号和出生日期可以用
类对象表示。定义三个类:学号类、日期类和学号类。学生类是一个组合类,类的数据
成员包括其他类对象,因此学生类的构造函数中使用初始化表初始化类对象数据成员。
【参考代码】
#include <iostream> #include <cstring> using namespace std; //日期类声明 class Date { public: Date(int y = 2021, int m = 1, int d = 1) { year = y; month = m; day = d; cout << "调用日期类的构造函数!" << endl; } Date(Date& s) { // 日期类的拷贝构造函数 year = s.year; month = s.month; day = s.day; cout << "调用日期类的拷贝构造函数!" << endl; } ~Date() { cout << "调用日期类的析构函数!" << endl; } private: int year; int month; int day; }; //学号类声明 class StudentID { public: StudentID(int i) { value = i; cout << "构造学号:" << value <<"的学号类构造函数!"<< endl; } ~StudentID() { cout << "析构学号:" << value <<"的学号类析构函数!"<< endl; } private: int value; }; //学生类声明 class Student { public: Student(char* na, char s, int i, Date& d); ~Student(); private: char name[20]; char sex; StudentID id; Date birthday; }; Student::Student(char* na, char s, int i, Date& d) : id(i), birthday(d) { strcpy(name, na); sex = s; cout << "调用学生" << name << "的构造函数!" << endl; } Student::~Student() { cout << "调用学生" << name << "的析构函数!" << endl; } int main() { Date day1(2022, 7, 31); Student stud1("张三", 'm', 20010702, day1);//定义学生对象 return 0; }
三、静态成员
有时某个类的所有对象都需要访问一个共享的数据。
1. 问题的引入
问题描述:一个有对象计数器的类,这个计数器对当前程序中共有多少个此类的对象进行计
数。计数方法很简单,创建对象时计数增加,删除对象时计数减少,这两个操作可以分别由构造函
数和析构函数完成。那么这个计数器应该用什么样的变量表示呢?
解决方案:
方法一:使用数据成员。每个对象都有一个独立的数据成员,那么这个计数器在每个对象中都存在
一个,当程序中对象数目发生改变时,要追踪和更新每个计数器很麻烦也很困难,并且也容易出现
不一致的错误。
方法二:使用全局变量计数。这种方法比在对象中使用计数器更高效。但是全局变量是可以被任何
对象或者函数访问或修改,不仅仅是这个类的对象,使得安全性不能保证。
类的静态数据成员为上述这个问题提供了更好的解决方按案。
静态数据成员被当作该类类型的全局变量。对非静态数据成员,每个对象都有自己的拷贝,而静态
数据成员对整个类只有一个,由这个类型的所有对象共享访问。
和全局变量相比,静态数据成员有以下两个优点:
(1)静态数据成员没有进入程序的全局作用域,只是在类作用域中,不会和全局作用域中的其它
名字产生冲突;
(2)可以实现信息隐藏,静态成员可以是 private 成员,而全局变量不行。
2. 静态数据成员
(1)静态数据成员的特点:
- 在每个类中只有一个拷贝,由该类的所有对象共同维护和使用;
- 静态数据成员的值对每个对象都是一样的,但是它的值是可以更新的;
- 只要对静态数据成员的值更新一次,保证所有对象存取更新后有相同的值。
(2)定义及初始化格式为:
static 类型标识符 静态数据成员名;
类型标识符 类名 :: 静态数据成员名 = 初始值;
注意:
a. 从逻辑角度上来讲,静态数据成员从属于类,而非静态数据成员从属于对象;
b. 从物理角度上来讲,静态数据成员存放在静态存储区,由所有对象共享,生命周期不依赖于对
象。而非静态数据成员独立存放于各个对象当中,生命周期依赖于对象,随对象的创建而存在,随
对象的销毁而消亡;
c. 初始化在类体外进行,而且前面不加 static,以免与一般静态变量相混淆;
d. 初始化时不加该成员的访问权限修饰符 private,public 等;
e. static 成员只能定义一次,所以定义一般不放在头文件中,而是放在包含成员函数定义的源文件
中。
(3)静态数据成员的访问
类名 :: 静态数据成员;
对象名. 静态数据成员;
Example:
【例】在 Student 类中添加静态数据成员,保存学生的总数。.
#include <iostream> #include <cstring> using namespace std; class Student { public: Student(int i, char* na); ~Student(); void Print(); static int count; //公有的静态数据成员 private: int id; char name[20]; }; Student::Student(int i, char* na) { id = i; strcpy(name, na); count++; //创建一个学生,学生数加1 } Student::~Student() { count--; //学生对象撤销时,学生数减1 } void Student::Print() { cout << "姓名:" << name << " ," << "学号:" << id << endl; } int Student::count = 0; //静态数据成员初始化 void display() { Student stud3(10003, "王五"); stud3.Print(); cout << "学生数为:" << Student::count << endl; } int main() { Student stud1(10001, "张三"); stud1.Print(); cout << "学生数为:" << Student::count << endl; //通过类名引用静态数据 Student stud2(10002, "李四"); stud2.Print(); cout << "学生数为:" << stud2.count << endl; //通过对象名引用静态数据 display(); cout << "学生数为:" << Student::count << endl; return 0; }
3. 静态成员函数
静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员,没有this 指针。
(1)静态成员函数的格式为:
static 函数类型 静态成员函数名(参数表);
静态成员函数是在成员函数声明的前面加上 static。
(2)调用静态成员函数格式为:
类名 :: 静态成员函数名(参数表);
对象名 . 静态成员函数名(参数表);
(3)静态成员函数的特点:
(1)对于公有的静态成员函数,可以通过类名或对象名来调用,而一般的非静态成员函数只
能通过对象名来调用。静态成员函数可以由类名通过符号“::”直接调用。
(2)静态成员函数可以直接访问该类的静态数据成员和静态成员函数,不能直接访问非静态
数据成员和非静态成员函数。如果静态成员函数中要引用非静态成员时,可通过对象来引
用。
【例】具有静态数据、函数成员的 Point 类
#include <iostream>
using namespace std;
class Point {
public:
Point(int X = 0, int Y = 0) {
x = X;
y = Y;
count++;
}
Point(Point& p); //拷贝构造函数
int GetX() {
return x;
}
int GetY() {
return y;
}
static void GetC() {
cout << "次数 = " << count << endl;
}
private:
int x, y;
static int count;
};
Point::Point(Point& p){
x = p.x;
y = p.y;
count++;
}
int Point::count = 0;
int main() {
Point A(4, 5); //声明对象A
cout << "Point A:(" << A.GetX() << "," << A.GetY() << ") " ;
A.GetC(); //输出对象号,对象名引用
Point B(A); //声明对象B
cout << "Point B:(" << B.GetX() << "," << B.GetY() << ") " ;
Point::GetC(); //输出对象号,类名引用
return 0;
}
4. 友元
(1)概念
友元是 C++ 提供的一种破坏数据封装和数据隐藏的机制。友元可以理解为是类的“朋友”,它
可以访问类的保护和私有成员。友元可以是一个函数,该函数被称为友元函数;友元也可以是一个
类,该类被称为友元类友元的作用在于提高程序的运行效率。但是,它破坏了类的封装性和隐藏
性,建议尽量不使用或少使用友元。
(2)友元函数
友元函数是在类外定义的一个函数,不是类的成员函数。这个函数可以是普通的 C++ 函数,
或者是其他类的成员函数。即普通友元函数和友元成员函数。友元函数是定义在类外部,但需要在
类体内进行说明,为了与该类的成员函数加以区别,在说明时前面加以关键字 friend。友元函数可
以访问类中的保护和私有成员。
友元函数的定义:
普通友元函数的格式为:
friend 类型标识符 友元函数名(参数列表);
友元成员函数(即将其它类的成员函数声明为该类的友元函数)的格式为:
friend 类型标识符 其它类名 :: 友元函数(参数列表);
Example:
【问题描述】
设计点类 Point,编写一个函数计算两点之间的距离。
【问题分析】
点类 Point 私有数据成员 x,y 表示点的坐标,定义成员函数 GetX() 和 GetY()分别得到点的
横坐标和纵坐标,定义一个普通函数计算两点间的距离时,得到两点的坐标值,计算距离。
【参考代码】
#include <iostream> #include <cmath> using namespace std; class Point { public: Point(int X = 0, int Y = 0); double GetX(); double GetY(); //声明普通函数为类的友元函数 friend double GetDistance(Point start, Point end); private: double x, y; }; Point::Point(int X, int Y) { x = X; y = Y; }; double Point::GetX() { return x; } double Point::GetY() { return y; } //友元函数 double GetDistance(Point start, Point end) { double d; //友元函数可以访问类的私有成员 d = sqrt((end.x - start.x) * (end.x - start.x) + (end.y - start.y) * (end.y - start.y)); return d; } int main() { Point p1(0, 0), p2(3, 4); double d; d = GetDistance(p1, p2); cout << "两点之间的距离是:" << d << endl; return 0; }
5. 友元类
一个类可以作另一个类的友元称为友元类。当一个类作为另一个类的友元类时,意味着这个
类的所有成员函数都是另一个类的友元函数,可以访问另一个类的私有成员。
声明友元类的格式为:
friend class 类名;
Example:
【问题描述】
定义一个日期类,包括年、月、日和小时、分钟、秒。
【问题分析】
首先定义一个时间类 Time,而日期类 Date 的数据成员包括年(year)月(month)日(day)和一个 Time 的类对象。日期类 Date 的成员函数 disaplay() 显示日期和时间,要访问 Time 类的私有成员,因此将日期类 Date 声明为时间类 Time 的友元类。
【参考代码】
#include <iostream> using namespace std; class Time { public: Time(int h = 0, int m = 0, int s = 0); /*声明Date类为Time类的友元类, **则Date中所有的成员函数都是Time类的友元函数, **可以访问Time类的私有成员或保护成员。 */ //声明友元类 friend class Date; private: int hour; int minute; int sec; }; Time::Time(int h, int m, int s) { hour = h; minute = m; sec = s; } class Date { public: Date(int y = 2021, int mon = 1, int d = 1, int h = 0, int m = 0, int s = 0); void disaplay(); private: int month, day, year; Time t; }; Date::Date(int y, int mon, int d, int h, int m, int s) :t(h, m, s) { month = mon; day = d; year = y; } void Date::disaplay() { cout << year << "/" << month << "/" << day<<" "; cout << t.hour << ":" << t.minute << ":" << t.sec << endl; } int main() { Date d1(2022, 7, 31, 10, 13, 56); d1.disaplay(); return 0; }
注意:
友元关系不具有交换性,是单向的;
友元关系不具有传递性。