3 类、对象和字符串的介绍
1 定义具有成员函数的类
GradeBook类
在main()
函数可以创建一个类的对象前,我们必须告诉编译器属于该类的成员函数和数据成员是什么。
#include<iostream>
using namespace std;
//GradeBook class definition
class GradeBook
{
public:
void displayMessage() const
{
cout << "Welcome to the Grade Book!" << endl;
}
}; //end class GradeBook
int main()
{
GradeBook myGradeBook; //creat a GradeBook object named myGradeBook
myGradeBook.displayMessage(); //call object's displayMessage function
}
按照惯例,用户自定义的类的名字以大写字母开头,且为增强可读性,类名中每个随后的单词其首字母也为大写。这种大写风格常称为Pascal风格(Pascal case),因为其广泛应用于Pascal程序设计语言中。更一般的==骆驼大写风格(camel case)==允许首字母既可以是小写也可以是大写。
每个类的体(body)包围在一对花括号中,类的定义以分号结束,忘记分号是个语法错误。
在执行程序时,main函数始终是自动调用的。而大多数函数不能自动调用,必须显式调用。
关键字public
是个成员访问说明符(access specifier)。出现在public
后的函数时“公共可用的”,可被程序中其他函数及其他类的成员函数调用。
定义函数时必须指定一个返回类型以说明函数完成它的任务时返回值的类型。void
返回类型说明函数将执行一个任务但是当完成任务时不向主调函数返回任何数据。
按照惯例,我们的函数名采用骆驼风格。
将成员函数声明为const
说明调用该函数时该函数不修改且不该修改调用它的对象。如果修改了会发出编译错误的信息。该代码块第八行称为函数的头部(function header)。
测试GradeBook类
main函数时每个程序执行的起点。
一般情况下,在没有创建类的对象前是不能调用类的成员函数的(static
成员函数例外)。
创建对象时,类就相当于一个数据类型。(这正是人们认为C++是个可扩展语言的原因之一)
上面例子在变量名(如myGradeBook
)后依次加点运算符(.
)、函数名(如displayMessage
)和空的圆括号对的方式调用成员函数。空括号对说明成员函数执行任务时不需要额外数据。
2 定义具有形参的成员函数
一个成员函数可以要求一个或多个形参(parameter),用于描述它执行任务所需要的额外数据。
对于函数的每一个形参,每次函数调用会给他们提供一个值,这些值称为实参(arguement)。
定义和测试GradeBook类
#include<iostream>
#include<string>
using namespace std;
//GradeBook class definition
class GradeBook
{
public:
void displayMessage(string courseName) const
{
cout << "Welcome to the Grade Book for\n" << courseName << "!" << endl;
}
}; //end class GradeBook
int main()
{
string nameOfCourse; //string of characters to store the course name
GradeBook myGradeBook; //creat a GradeBook object named myGradeBook
cout << "Please enter the course name:" << endl;
getline(cin, nameOfCourse); //read a course name with blanks
cout << endl;
//call object's displayMessage function
//and pass nameOfCourse as a argument
myGradeBook.displayMessage(nameOfCourse);
}
类型为string
的变量表示一串字符。一个字符串实际上是C++标准库string
类的一个对象,这个类在头文件<string>
中定义。且string
这个名字属于名字空间std。
getline
库函数被调用时从标准输入流对象cin(即键盘)连续读取字符(包括分隔输入中的单词的空格),直到遇到换行符为止。读取的这些字符将放入 string
变量nameOfCourse
中并丢弃换行符。(在输入数据的过程中按下回车键时,会将一个换行符插入到输入流中)。
使用getline
函数必须包含<string>
头文件,且getline
这个名字属于名字空间。
实参和形参的进一步讨论
形参列表位于函数名之后的圆括号里,可以包含任意多个形参,或者根本没有形参。
对于每个形参而言,应当指定其类型和标识符。
形参变量的名字可以与实参变量的名字同名,也可以不同名。
函数可以用逗号分隔前后形参的方法指定多个形参。
在函数调用中实参的个数和顺序,必须与被调用函数头部的形参列表中形参的个数和顺序相匹配。实参的类型也要和函数头部相应形参的类型相符(不必总是一样,但必须“相容”)。
3 数据成员、set成员函数和get成员函数
声明在一个函数定义体内的变量被认为是局部变量(local variable),且必须在使用前声明,不能在声明它们的函数外部被访问。当函数结束时,其局部变量的值将丢失(static
局部变量例外)。
类通常由一或多个成员函数组成,这些成员函数操作属于该类的某个特定对象的属性。在类的定义中属性表示为变量,这样的变量称为数据成员(data member),声明在类的定义之内,但在所有类的成员函数的定义体之外。
类的每个对象管理它自己在内存中的属性。这些属性在对象的整个生命期一直存在。
若创建一个类的多个对象,每个对象都有它自己的数据成员,可拥有不同的值。
具有一个数据成员、一个set成员函数和一个get成员函数的GradeBook类
#include<iostream>
#include<string>
using namespace std;
//GradeBook class definition
class GradeBook
{
public:
void setCourseName(string name)
{
courseName = name;
}
string getCourseName() const
{
return courseName;
}
void displayMessage(string courseName) const
{
cout << "Welcome to the Grade Book for\n" << getCourseName() << "!" << endl;
}
private:
string courseName;
}; //end class GradeBook
int main()
{
string nameOfCourse; //string of characters to store the course name
GradeBook myGradeBook; //creat a GradeBook object named myGradeBook
cout << "Initial course name is: " << myGradeBook.getCourseName() << endl;
cout << "\nPlease enter the course name:" << endl;
getline(cin, nameOfCourse); //read a course name with blanks
myGradeBook.setCourseName(nameOfCourse); //set the course name
}
GradeBook类的每个对象都包含了类的每个数据成员的一份副本。
成员访问说明符public和private
绝大多数数据成员的声明出现在成员访问说明符private
之后。在成员访问说明符private
之后(且在下一个成员访问说明符之前)声明的变量或者函数,只可以被==声明它们的类的成员函数或者类的“友元”==所访问。
类成员默认的成员访问说明符是private
,因此类头部之后、第一个成员访问说明符之前的所有成员函数都是私有的。利用成员访问说明符private
声明数据成员被视为数据隐藏(data hiding)。
默认情况下,一个字符串的初始值是所谓的“空串(empty string)”,也就是不包含任何字符的字符串。当显示空串时,屏幕上不出现任何内容。
4 使用构造函数初始化对象
声明的每个类都可以提供一到多个构造函数(constructor),用于类对象创建时它的初始化。
构造函数是一种特殊的成员函数,定义时必须和类同名,这样编译器才能够将它和类的其他成员函数区分开来。
构造函数和其他函数之间的一个重大差别是构造函数不能返回值,因此对他们不可以指定返回类型(即使是void
)。
通常情况下,构造函数声明为public
。
对于每个创建的对象,C++自动调用构造函数,即构造函数的调用发生在对象创建时。
在任何没有显式地包含构造函数的类中,编译器会提供一个默认的构造函数,更确切的说,是一个没有形参的构造函数。
#include<iostream>
#include<string>
using namespace std;
//GradeBook class definition
class GradeBook
{
public:
//construtor initializes courseName with string supplied as argument
explicit GradeBook(string name)
:courseName(name) //member initialzer to initialize courseName
{
//empty body
} //end GradeBook constructor
void setCourseName(string name)
{
courseName = name;
}
string getCourseName() const
{
return courseName;
}
void displayMessage(string courseName) const
{
cout << "Welcome to the Grade Book for\n" << getCourseName() << "!" << endl;
}
private:
string courseName;
}; //end class GradeBook
int main()
{
//creat two GradeBook objects
GradeBook gradeBook1("CS101 Introduction to C++ Programming");
GradeBook gradeBook2("CS101 Introduction to C++ Programming");
//display initial value of courseName for each GradeBook
cout << "gradeBook1 created for course: " << gradeBook1.getCourseName() << "\ngradeBook2 created for course: " << gradeBook2.getCourseName() << endl;
}
定义构造函数
构造函数不能声明为const
,因为对象的初始化修改了对象。
构造函数通过成员初始化列表用构造函数形参name
的值初始化数据成员courseName
。
如果类包含多个数据成员,每个数据成员的初始化项用逗号前后隔开。
为类提供默认构造函数的方法
任何不接受实参的构造函数,称为默认的构造函数。类通过下面的方法之一得到默认的构造函数:
- 编译器隐式地在没有任何用户自定义的构造函数的类中创建一个默认的构造函数。这样的默认构造函数一般不初始化类的数据成员,但是如果数据成员是其他类的对象,那么这个类的默认构造函数会调用这此数据成员的默认构造函数。没有初始化的变量通常包含未定义的“垃圾”值。
- 程序员显式定义一个不接受实参的构造函数。这样的默认构造函数将调用是其他类对象的每个数据成员的默认构造函数,并执行程序员规定的其他初始化任务。
- 如果程序员定义任何具有实参的构造函数,C++将不再为这个类隐式地创建一个默认的构造函数。不过即使已经定义了非默认的构造函数,C++11仍允许大家强行令编译器去创建默认的构造函数。
5 一个类对应一个独立文件的可复用性
头文件
前面例子都由包含了GradeBook的类定义和一个main函数的单个.cpp
文件(也称为一个源代码文件)构成。
当构建面向对象的C++程序时,通常在一个文件中定义可复用源代码(如一个类),按照惯例这个文件扩展名为.h
——称为头文件(header file)。
//GradeBook.h
//GradeBook class definition in a separate file from main
#include<iostream>
#include<string>
//GradeBook class definition
class GradeBook
{
public:
//construtor initializes courseName with string supplied as argument
explicit GradeBook(std::string name)
:courseName(name) //member initialzer to initialize courseName
{
//empty body
} //end GradeBook constructor
void setCourseName(std::string name)
{
courseName = name;
}
std::string getCourseName() const
{
return courseName;
}
void displayMessage(std::string courseName) const
{
std::cout << "Welcome to the Grade Book for\n" << getCourseName() << "!" << std::endl;
}
private:
std::string courseName;
}; //end class GradeBook
头文件不应该包含using
指令或者using
声明,所以上面代码引用string
、cout
、endl
时使用了std::
。
//.cpp
#include<iostream>
#include"GradeBook.h"
using namespace std;
//function main begins program execution
int main()
{
//creat two GradeBook objects
GradeBook gradeBook1("CS101 Introduction to C++ Programming");
GradeBook gradeBook2("CS101 Introduction to C++ Programming");
//display initial value of courseName for each GradeBook
cout << "gradeBook1 created for course: " << gradeBook1.getCourseName() << "\ngradeBook2 created for course: " << gradeBook2.getCourseName() << endl;
}
包含一个用户自定义类的头文件
虽然概念上对象包含数据成员和成员函数,但是C++对象实际上只包含数据。编辑器仅创建类的成员函数的一份副本,该类所有的对象共享它。
因此,一个对象的大小依赖于存储类的数据成员所需的内存大小。
在上面代码中通过包含GradeBook.h
,使编译器获知确定一个GradeBook
对象的大小所需要的信息,以及确定类的对象是否被正确使用的信息。
如何找到头文件
头文件的名字括在双引号""
中而非尖括号<>
中。
正常情况下,程序源代码文件和用户自定义的头文件放在同一个文件夹下。当处理器遇到括在双引号的头文件名时,它就会试着在该#include
指令出现的文件所在的文件夹下寻找改头文件。若预处理器在此文件夹下未能找到这个头文件,那么它会继续在C++标准库头文件所在的文件夹下搜索。当预处理器遇到括在尖括号中的头文件名(如<iostream>
)时,它认为这个头文件时C++标准库的一部分,所以不会到正被预处理的这个程序所在的文件夹中取查找。
6 接口的实现与分离
上一节将类定义与使用该类的客户代码(如main函数)分离。下面介绍分离类的接口与类的实现。
类的接口
类的接口(interface)描述了该类的客户所能使用的服务,以及如何请求这些服务,但不描述类如何实现这些服务。
类的接口由类的public
成员两数(也称为类的公共服务)组成。例如,GradeBook类的接口包含一个构造函数及成员函数setCourseName 、getCourseName和displayMessage。GradeBook 的客户(例如main函数)使用这些函数请求类的服务。
通过书写只列出成员函数名、返回类型和形参类型的类定义,就可以说明一个类的接口。
分离接口与实现
下面将类定义分成两个文件——定义GradeBook类的头文件GradeBook.h
和定义GradeBook成员函数的源代码文件GradeBook.cpp
,将类的接口从它的实现中分离出来。
按照惯例,成员函数的定义放在一个与类的头文件基本名(如GradeBook)同名而文件扩展是.cpp
的源代码文件中。
GradeBook.h :使用函数原型定义类的接口
//GradeBook.h
//GradeBook class definition. This file presents GradeBook's public
//interface without revealing the implementations of GradeBook's member
#include<string>
//GradeBook class definition
class GradeBook
{
public:
explicit GradeBook(std::string);//constructor initialize courseName
void setCourseName(std::string);
std::string getCourseName() const;
void displayMessage() const;
private:
std::string courseName; //course name for this GradeBook
}; //end class GradeBook
上面代码中原本的函数定义被函数原型(function prototype)所替换,它们描述了类的公共接口而没有暴露类的成员函数的实现。
函数原型是函数的声明,告诉编译器函数的名字、返回类型和形参的类型。
GradeBook.cpp :在独立的源代码文件中定义成员函数
//GradeBook.cpp
//GradeBook member-function definitions. This file contains
// implementations of the member functions prototyped in GradeBook.h
#include<iostream>
#include"GradeBook.h"
using namespace std;
//construtor initializes courseName with string supplied as argument
GradeBook::GradeBook(string name)
:courseName(name) //member initialzer to initialize courseName
{
//empty body
} //end GradeBook constructor
void GradeBook::setCourseName(string name)
{
courseName = name;
}
string GradeBook::getCourseName() const
{
return courseName;
}
void GradeBook::displayMessage() const
{
std::cout << "Welcome to the Grade Book for\n" << getCourseName() << "!" << endl;
}
在函数头部,每个成员函数名之前都添加了类名和符号::
,该符号是作用域分辨运算符(scope resolution operator)。这样将每个成员函数“捆绑”到目前分开的GradeBook的类定义上,该类定义声明了类的成员函数和数据成员。
若没有GradeBook::
,编译器将不承认这些函数是GradeBook类的成员函数——编译器将认为他们是“自由的”“无拘束的”函数,就像main函数一样。这样的函数也称作全局函数。这种没指定对象的函数不可以访问GradeBook的private
数据,或者调用类的成员函数,所以编译器不能编译这些函数。
测试GradeBook类
客户代码中成员函数的调用需要和类的成员函数的实现捆绑在一起。这是一项由链接器完成的工作。
//.cpp
#include<iostream>
#include"GradeBook.h"
using namespace std;
//function main begins program execution
int main()
{
//creat two GradeBook objects
GradeBook gradeBook1("CS101 Introduction to C++ Programming");
GradeBook gradeBook2("CS102 Data Structures in C++");
//display initial value of courseName for each GradeBook
cout << "gradeBook1 created for course: " << gradeBook1.getCourseName() << "\ngradeBook2 created for course: " << gradeBook2.getCourseName() << endl;
}
编译和链接过程
为隐藏GradeBook类成员函数的实现细节,类实现程序员将向客户代码程序员提供头文件GradeBook.h
和GradeBook类的目标代码。目标代码包含了描述GradeBook成员函数的机器指令。GradeBook.cpp
的源代码文件并未提供给客户代码程序员,所以客户依旧不知道GradeBook成员函数是如何实现的。
客户代码只需要知道使用GradeBook类的接口,并且能够链接到它的目标代码。由于类接口是 GradeBook.h
头文件中类定义的一部分,客户代码程序员必须有权访问这个文件并且包含(#include
)它。
为创建可执行的GradeBook
应用程序,最后步骤是链接如下几个部分:
- main函数的目标代码(即客户代码)
- GradeBook类成员函数实现的目标代码
- 类实现程序员和客户代码程序员使用的C++类(如
string
)的C++标准库目标代码
7 用set函数确认数据的有效性
若我们要求类GradeBook能够保证它的数据成员courseName从不超过25个字符,下面我们增强GradeBook类的成员函数setCourseName,让它执行数据有效性确认(也称为有效性检查)的工作。
GradeBook的类定义
接口同先前的一模一样,即
//GradeBook.h
//GradeBook class definition. This file presents GradeBook's public
//interface without revealing the implementations of GradeBook's member
#include<string>
//GradeBook class definition
class GradeBook
{
public:
explicit GradeBook(std::string);//constructor initialize courseName
void setCourseName(std::string);
std::string getCourseName() const;
void displayMessage() const;
private:
std::string courseName; //course name for this GradeBook
}; //end class GradeBook
仅需简单地通过将客户代码链接到更新后的GradeBook目标代码,从而利用改进后的GradeBook类。
使用GradeBook成员函数setCourseName确认课程名称的有效性
现在构造函数调用setCourseName成员函数,而没有使用成员初始化项。首先,在构造函数的体执行之前,courseName的值将设置为空串,然后setCourseName成员函数修改courseName的值。
//GradeBook.cpp
//GradeBook member-function definitions. This file contains
// implementations of the member functions prototyped in GradeBook.h
#include<iostream>
#include"GradeBook.h"
using namespace std;
//construtor initializes courseName with string supplied as argument
GradeBook::GradeBook(string name)
{
setCourseName(name); //validate and store courseName
} //end GradeBook constructor
void GradeBook::setCourseName(string name)
{
if (name.size() <= 25)
{
courseName = name; //store the course name in the object
}
if (name.size() > 25)
{
//set courseName to first 25 characters of parameter name
courseName = name.substr(0, 25); //start at 0, length of 25
cerr << "Name \"" << name << "\"exceeds maximum length(25).\n" << "Limiting courseName to first 25 characters.\n" << endl;
}
}
string GradeBook::getCourseName() const
{
return courseName;
}
void GradeBook::displayMessage() const
{
std::cout << "Welcome to the Grade Book for\n" << getCourseName() << "!" << endl;
}
C++标准库的string
类定义了成员函数size,它返回string
对象中的字符个数。
标准类string
提供了成员函数substr
,它返回一个新的string对象,该对象是通过复制已存在string对象的一部分而创建的。
关于set函数的补充说明
#include<iostream>
#include"GradeBook.h"
using namespace std;
//function main begins program execution
int main()
{
//creat two GradeBook objects
GradeBook gradeBook1("CS101 Introduction to C++ Programming");
GradeBook gradeBook2("CS102 Data Structures in C++");
//display initial value of courseName for each GradeBook
cout << "gradeBook1's initial course name is: " << gradeBook1.getCourseName() << "\ngradeBook2's initial course name is: " << gradeBook2.getCourseName() << endl;
//modify gradeBook1's courseName(with a valid-length string)
gradeBook1.setCourseName("CS101 C++ Programming");
cout << "gradeBook1's course name is: " << gradeBook1.getCourseName() << "\ngradeBook2's course name is: " << gradeBook2.getCourseName() << endl;
}
运行后发现类的set函数可以返回一个值,说明已有对类的对象赋无效数据的尝试发生。
像setCourseName之类的公有set函数,应当仔细审查对数据成员(如courseName)值的任何修改尝试,以保证新的值适合该数据项。