第1关:构造函数 —— 学生信息类

任务描述

本关任务:设计一个带有构造函数和析构函数的学生类。

相关知识

构造函数、析构函数与赋值函数是每个类最基本的函数。他们太普通以致让人容易麻痹大意,其实这些貌似简单的函数在使用时要特别注意以免造成不必要资源浪费和产生意想不到的错误。

每个类只有一个析构函数和一个赋值函数,但是可以有多个构造函数(包含一个拷贝构造函数,其他的成为普通构造函数)。

下面我们就一起来学习构造函数和析构函数的基本使用。

构造函数

所谓构造函数,就是在对象构造的时候调用的函数。构造函数是一种特殊的成员函数,它主要用于为对象分配空间,进行初始化。

构造函数在定义类对象时自动调用,不需用户调用,也不能被用户调用。在对象使用前调用。如果类中没有定义构造函数,系统则会自动给出一个无参构造函数。

构造函数没有返回值,函数名必须与类名一致,一个类可以有多个构造函数,但是参数必须有差别(也就是所谓的重载)。

例如:

class Test
{
    public:
        Test();     // 无参数的构造函数
        Test(int a);     // 有一个 int 参数的构造函数
    private:
        Test(int a,int b);     // 私有的两个参数的构造函数
};
Test::Test()
{ /* 此处省略一些初始化的工作 */}
Test::Test(int a)
{ /* …… */}
Test::Test(int a,int b)
{ /* …… */}

构造函数也会受访问性影响,在不同的作用范围,能调用的构造函数也会不同。

初始化成员

构造函数的一个重要任务就是给成员初始化,初始化成员有两种办法,一种是手动给成员赋值,另一种是使用初始化列表。这里介绍第二种,格式为:



  1. 类名::构造函数名(参数表): (成员初始化表){ 构造函数体 }

构造函数中的初始化列表只需要在参数列表的后面加一个冒号:),然后将要初始化的成员按照成员名(参数)的格式排列在后面,个成员之间用逗号隔开。

例如:

class Test
{
public:
    int A;
    int B;
    Test(int a);
};
Test::Test(int a)
    :A(a),B(10)     //给成员变量 A、B 初始化,不一定要和参数列表写在一行
{ /* …… */ }

其中成员的初始化顺序不是按照初始化列表中的顺序来的,而是按照成员声明的顺序来的,例如:

/* Test类的声明同上 */
Test::Test(int a)
    :B(10),A(a)     // 虽然 B 在前面,但还是 A 先初始化
{/* …… */}
Test::Test(int a)
    :B(a),A(B)     //此处 A 的初始化依赖了 B,然而是 A 先初始化,这就导致 A 得到了 B 中还没初始化的错误内容
{/* …… */}

析构函数

析构函数是一种特殊的成员函数,它会在每次删除所创建的对象时执行。它执行与构造函数相反的操作,通常用于撤消对象时的一些清理任务,有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。格式如下:



  1. 类名::~析构函数名(){}

例如:

class Test
{
    public:
        ~Test();     // 析构函数
};
Test::~Test()
{/* 一些收尾的工作 */}

编程要求

在右侧编辑器中的Begin-End之间补充代码,编写一个学生类 Student,类中含有两个成员变量、两个构造函数和一个析构函数(访问性都为公有的),平台会调用你编写的函数来创建学生对象,具体要求如下:

  • 学号:int SID
  • 姓名:string Name
  • 无参的构造函数:Student(),在函数中将学号初始化为0,姓名初始化为王小明。
  • 带两个参数的构造函数:Student(int sid,string name);,在函数中分别用这两个参数设置内部的两个成员。
  • 析构函数:~Student(),在函数中输出学号 姓名 退出程序的消息。

测试说明

平台会对你编写的代码进行测试,比对你输出的数值与实际正确数值,只有所有数据全部计算正确才能通过测试:

测试输入:1 厉宏富 2 冷欣荣 3 鲍俊民

预期输出:


1. 0 王小明 退出程序
2. 3 鲍俊民 退出程序
3. 2 冷欣荣 退出程序
4. 1 厉宏富 退出程序

开始你的任务吧,祝你成功!

#include<iostream>
#include<string>
using namespace std;

class Student
{
/********* Begin *********/

//在此处声明所需的成员
public:
    int SID;
    string Name;
    Student();
    Student(int sid, string name);
    ~Student();
/********* End *********/
};

/********* Begin *********/

//在此处定义成员函数
Student::Student(){
    SID = 0;
    Name = "王小明";
}
Student::Student(int sid, string name){
    SID = sid;
    Name = name;
}
Student::~Student(){
    cout<<SID<<" "<<Name<<" "<<"退出程序"<<endl;
}
/********* End *********/

第2关:对象数组 —— 学生信息表

任务描述

本关任务:编写一个能管理多条学生信息的程序。

相关知识

为了完成本关任务,你需要掌握构造函数与析构函数的调用和对象数组的使用。

构造函数与析构函数的调用

构造函数不能直接调用,只能通过声明一个对象或者使用new 运算符动态创建对象时由系统自动调用。

例如:

class Test
{
    public:
        int A;
        Test();
        Test(int a);
};
/* 此处省略定义构造函数部分 */
int main()
{
    Test t;     // 调用无参构造函数
    Test t2(10);     // 调用带参构造函数
    Test t3 = Test(10);     // 同上
    Test *t = new Test;     // 动态创建对象,调用无参构造函数
    Test *t2 = new Test(10);     // 动态创建对象,调用带参构造函数
}

而析构函数则不同,它能够通过对象主动调用,并在以下两种情况下它会自动调用:

  1. 若一个对象被定义在一个函数体内,当这个函数结束时(声明的变量的生命周期结束)会自动调用。
  2. 若一个对象是使用 new 运算符动态创建,在使用 delete 释放时会自动调用。

例如:

/* Test类的声明接上 */
Test::~Test()     // 修改一下析构函数,让它打印一条消息
{
    cout << "Test的析构函数被调用" << endl;
}
int main()
{
    cout << "p1" << endl;
    {
        Test t1(1);     // t1 的生命周期就只在这个大括号内
    }     //因此在这个位置 t1 的析构函数就会被调用
    cout << "p2" << endl;
    Test *t2 = new Test(10);
    delete t2;     // t2 所指对象的析构函数在此被调用
    cout<<"p3"<<endl;
    {
        Test *t3 = new Test;
    }    // t3 所指对象的析构函数并不会被调用,因为没有使用 delete 运算符
}

输出结果为:



  1. p1
  2. Test的析构函数被调用
  3. p2
  4. Test的析构函数被调用
  5. p3

上述代码中 t1 对象的析构函数调用的位置有点微妙,它是在代码离开大括号}那瞬间的位置被调用的,因为一个变量只在直接包含它的那层大括号的范围内存活。

对象数组

数组对象就是大批量实例化对象的一种方法,以往我们都是这样:Student stu实例化对象,如果有好几百个对象应该怎么办?

这时候就用到了对象数组,顾名思义,就是把所有要实例化的对象都放到一个组里面,然后直接实例化这个组,就像这样:Student stu[100],便可一次性实例化100个对象。

对象数组与一般的数组基本一致,只是多了两个过程:

  1. 在数组创建的时候对数组的每一个元素都调用了构造函数;
  2. 在数组生命结束的时候对数组的每一个元素都调用了析构函数。

如果使用 new 运算符来动态创建对象数组,也是同样的过程。

注意:在创建数组时如果不使用列表初始化语法对数组中的每一个元素调用构造函数,那么默认调用无参数的构造函数,因此也就要求这个类必须要有无参数的构造函数。

例如:

class Test1
{
    public:
        Test1();
        ~Test1();
};
Test1::Test1()
{
    cout << "Test1的构造函数" <<endl;
}
Test1::~Test1()
{
    cout << "Test1的析构函数" <<endl;
}
class Test2
{
    public:
        Test2(int a);     // 没有无参数的构造函数
        ~Test2();
};
Test2::Test2(int a)
{
    cout << "Test2的构造函数" <<endl;
}
Test2::~Test2()
{
    cout << "Test2的析构函数" <<endl;
}
int main()
{
    Test1 ts1[2];     // Test1 有无参构造函数,OK
    Test2 ts2[2];     // 错误,Test2 没有无参构造函数
    Test2 ts3[2]={Test2(10)};     // 这个也错误,因为只对第一个元素调用了构造函数,第二个还是会主动调用无参构造函数
    Test2 ts4[2]={Test2(10),Test2(20)};    // 正确
}

如果删除那两行错误的声明,那么输出结果为:



  1. Test1的构造函数
  2. Test1的构造函数
  3. Test2的构造函数
  4. Test2的构造函数

  5. Test2的析构函数
  6. Test2的析构函数
  7. Test1的析构函数
  8. Test1的析构函数

为了方便查看,在输出结果中间空了一行,上面是创建数组时对元素调用构造函数产生的输出,下面是数组死亡时对每一个元素调用析构函数产生的输出。

编程要求

在右侧编辑器中的Begin-End之间补充代码,设计 Student 类并实现用于管理学生信息表(学生表的长度不超过5)的3个函数,成员变量和函数的访问性都为公有的,具体类结构和函数要求如下:

  • 学号,int类型
  • 姓名,string类型
  • 分数,float类型
  • 带参构造函数:Student(int sid,string name,float sco),分别用这三个参数设置内部的三个成员。
  • void Add(int sid,string name,float sco),函数用于向学生表的末尾添加一条学生记录。
  • void PrintAll(),输出学生表中所有的记录,格式为:学号 姓名 成绩
  • void Average(),计算学生表中学生的平均成绩并输出,格式为:平均成绩 计算结果

提示:学生表可以用全局对象数组来完成,定义全局对象数组和定义全局变量一样,即定义在最外层作用域。

测试说明

平台会对你编写的代码进行测试,比对你输出的数值与实际正确数值,只有所有数据全部计算正确才能通过测试:

测试输入:0 厉宏富 96 1 冷欣荣 85 2 鲍俊民 76

预期输出:



  1. 0 厉宏富 96
  2. 1 冷欣荣 85
  3. 2 鲍俊民 76
  4. 平均成绩 85.6667

开始你的任务吧,祝你成功!

#include <string>
#include <iostream>
using namespace std;

/********* Begin *********/
class Student
{
	//在此处声明所需的成员
    public:
        int SID;
        string Name;
        float Score;
        Student();
        Student(int sid,string name,float sco);
        void Add(int sid,string name,float sco);
        void PrintAll();
        void Average();
    
    
};
/********* End *********/
Student::Student(){

}
Student::Student(int sid,string name,float sco){
    SID = sid;
    Name = name;
    Score = sco;
}
Student s[20];
int count = 0;
void Add(int sid,string name,float sco)
{
    /********* Begin *********/
    s[count] = Student(sid, name ,sco);
    count++;
    
    
    /********* End *********/
}

void PrintAll()
{
    /********* Begin *********/
    //打印出学生表中所有记录
    for(int i = 0;i < count;i++){
        cout<<s[i].SID<<" "<<s[i].Name<<" "<<s[i].Score<<endl;
    }
    
    
    /********* End *********/
}

void Average()
{
    /********* Begin *********/
    //计算并打印出学生表中的平均成绩
    double sum = 0;
    for(int i = 0;i < count;i++){
        sum += s[i].Score;
    }
    double ave = sum / count;
    cout<<"平均成绩 "<<ave<<endl;
    
    
    /********* End *********/
}

第3关:静态成员 —— 模拟共享书店

任务描述

本关任务:假设有一个这样的共享书店,当客户进入书店时需要托管一定数量的书籍,而这些书籍将被书店内所有用户共享,当客户离开书店时,他便会把之前进入书店托管的书一起带走。现请你编写代码设计这个共享书店。

相关知识

对象的内存中包含了成员变量,不同的对象占用不同的内存,这使得不同对象的成员变量相互独立,它们的值不受其他对象的影响。例如有两个相同类型的对象 a、b,它们都有一个成员变量 name,那么修改 a 对象的 name 值不会影响 b 中的 name 值。

可是有时候我们希望在多个对象之间共享数据,即对象 a 改变了某份数据后对象 b 可以检测到。共享数据的典型使用场景是计数。在 C++ 中,我们可以使用静态成员变量来实现多个对象共享数据的目标。

下面我们就一起之来学习静态成员的声明、定义及使用。

静态成员

静态成员变量是一种特殊的成员变量,它用关键字 static 来修饰。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本,对它做出修改时所有对象都是可见的。

静态成员在类的所有对象中是共享的。声明一个静态成员与声明一个非静态成员(也叫实例成员)基本一致,只需要在声明的最前面加上一个 static 关键字即可。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。

例如:

class Test
{
public:
    int A;     // 实例成员
    static int B;     // 静态成员变量
    static void Fun1();     // 静态成员函数
};

注意:静态成员也是有访问性的。

定义分成两部分,一是静态变量的初始化,二是静态函数的定义

静态变量的初始化不能在类的定义中,但是可以在类的外部通过使用范围解析运算符::来重新声明静态变量从而对它进行初始化。而定义静态函数,那就与定义实例函数一样了。

例如:

class Test
{
public:
    static string HelloStr;
    static void Hello();
    void World();
};
string Test::HelloStr = "Hello";     // 静态变量初始化
void Test::World()     // 定义实例成员函数
{
    cout<<"World"<<endl;
}
void Test::Hello()     // 定义静态函数,与定义 World 函数形式一样
{
    cout<<"Hello"<<endl;
}

访问静态成员

静态成员的访问有以下两种方法:

  1. 使用类型名::静态成员格式访问;
  2. 通过对象访问。

第一种访问方式可以将其看做是一个全局变量,只不过变量名要带上类型名::的前缀;第二种可以将其看做是对象中的一个实例成员。

例如:

/* Test类的定义同上文 */
int main()
{
    cout << Test::HelloStr << endl;     // 通过作用域运算符访问
    Test::HelloStr = "World";     // 修改静态变量 HelloStr
    Test t1;
    cout << t1.HelloStr <<endl;     // 通过对象访问
    Test::Hello();     // 通过作用域运算符访问
    t1.World();     // 通过对象访问
}

输出结果为:



  1. Hello
  2. World
  3. Hello
  4. World

注意第一、二行输出的差别,那是因为代码修改了所有对象共享的Test::HelloStr静态变量。

编程要求

在右侧编辑器中的Begin-End之间补充代码,设计一个 User 类(客户类),现有一个共享书店,该书店客户在进入书店时需要托管一定量的书籍,而这些书籍将由书店内所有用户共享,当客户离开书店时,他还是要将他进入书店时托管的书带走。设计时访问性可自主选择,具体要求如下:

  • 姓名:string Name
  • 托管的书籍量:int Books
  • 带参构造函数:User(string name,int books),使用这两个参数初始化内部的两个成员,同时按照姓名 数量 进入的格式打印一条消息。
  • 析构函数:~User(),按照姓名 数量 离开的格式打印一条消息。
  • 静态成员函数:void GetState(),按照书店人数:用户总数,书店共享书数量:书籍总数,人均共享数量:人均书籍量的格式打印一条消息,其中人均书籍量只保留整数部分,具体请参考测试说明。

提示:可以增加 UserCount ,BookCount 两个静态变量用于记录已有用户数和已有书籍数。

测试说明

平台会对你编写的代码进行测试,比对你输出的数值与实际正确数值,只有所有数据全部计算正确才能通过测试:

测试输入:厉宏富 10 冷欣荣 2 叶文光 0

预期输出:



  1. 厉宏富 10 进入
  2. 冷欣荣 2 进入
  3. 书店人数:2,书店共享书数量:12,人均共享数量:6
  4. 厉宏富 10 离开
  5. 叶文光 0 进入
  6. 书店人数:2,书店共享书数量:2,人均共享数量:1
  7. 冷欣荣 2 离开
  8. 书店人数:1,书店共享书数量:0,人均共享数量:0
  9. 叶文光 0 离开

开始你的任务吧,祝你成功!

#include <string>
#include <iostream>
using namespace std;

/********* Begin *********/
class User
{
	//在此处声明所需的成员
    public:
        static int UserCount;
        static int BookCount;
        string Name;
        int Books;
        User();
        User(string name, int books);
        ~User();
        
        static void GetState();

    
    
};

//在此处定义成员函数
int User::UserCount = 0;
int User::BookCount = 0;
User::User(){

}
User::User(string name, int books){
    Name = name;
    Books = books;
    cout<<Name<<" "<<Books<<" 进入"<<endl;
    UserCount++;
    BookCount += books;
}
User::~User(){
    cout<<Name<<" "<<Books<<" 离开"<<endl;
    UserCount--;
    BookCount -= Books;
}

void User::GetState(){
    cout<<"书店人数:"<<UserCount<<","<<"书店共享书数量:"<<BookCount<<","<<"人均共享数量:"<<BookCount / UserCount<<endl;
}

/********* End *********/