关于类
1. 类的基本结构
通常来说,我们将类的定义分为两个部分——声明和具体实现,将这两个部分分别放在.h和.cpp文件中,通过包含头文件的方式将.h文件与其他文件链接起来,通过cmake将.cpp文件与主函数链接起来
类的基本声明格式如下:
通常来说,根据oop的原则,将用户所需要的功能和接口定义在public中,而剩下的数据以及实现所需要的其他函数定义在private中。
class 类名
{
private:
//此部分为私有的数据或函数,不能被外部访问或调用,只能在这个类内部访问或调用
public:
//此部分为公开部分,是与外界的接口,可以被外界访问或调用
};
以下是一个具体的例子:
(CPP第十章P365"stock20.h"):
// stock20.h -- augmented version
#ifndef STOCK20_H_
#define STOCK20_H_
#include <string>
class Stock
{
private:
std::string company;
int shares;
double share_val;
double total_val;
void set_tot() {
total_val = shares * share_val; }
public:
// Stock(); // default constructor
Stock(const std::string & co, long n = 0, double pr = 0.0);
~Stock(); // do-nothing destructor
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show()const;
const Stock & topval(const Stock & s) const;
};
#endif
以下为各个成员函数的具体实现:
(CPP第十章P366"stock20.cpp"):
// stock20.cpp -- augmented version
#include <iostream>
#include "stock20.h"
using namespace std;
// constructors
Stock::Stock() // default constructor
{
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
Stock::Stock(const std::string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
// class destructor
Stock::~Stock() // quiet class destructor
{
}
// other methods
void Stock::buy(long num, double price)
{
if (num < 0)
{
std::cout << "Number of shares purchased can't be negative. "
<< "Transaction is aborted.\n";
}
else
{
shares += num;
share_val = price;
set_tot();
}
}
void Stock::sell(long num, double price)
{
using std::cout;
if (num < 0)
{
cout << "Number of shares sold can't be negative. "
<< "Transaction is aborted.\n";
}
else if (num > shares)
{
cout << "You can't sell more than you have! "
<< "Transaction is aborted.\n";
}
else
{
shares -= num;
share_val = price;
set_tot();
}
}
void Stock::update(double price)
{
share_val = price;
set_tot();
}
void Stock::show() const
{
using std::cout;
using std::ios_base;
// set format to #.###
ios_base::fmtflags orig =
cout.setf(ios_base::fixed, ios_base::floatfield);
std::streamsize prec = cout.precision(3);
cout << "Company: " << company
<< " Shares: " << shares << '\n';
cout << " Share Price: $" << share_val;
// set format to #.##
cout.precision(2);
cout << " Total Worth: $" << total_val << '\n';
// restore original format
cout.setf(orig, ios_base::floatfield);
cout.precision(prec);
}
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s;
else
return *this;
}
以下为使用示例:
(CPP第十章P369"usesotck2.cpp"):
// usestok2.cpp -- using the Stock class
// compile with stock20.cpp
#include <iostream>
#include "stock20.h"
const int STKS = 4;
int main()
{
{
// create an array of initialized objects
Stock stocks[STKS] = {
Stock("NanoSmart", 12, 20.0),
Stock("Boffo Objects", 200, 2.0),
Stock("Monolithic Obelisks", 130, 3.25),
Stock("Fleep Enterprises", 60, 6.5)
};
std::cout << "Stock holdings:\n";
int st;
for (st = 0; st < STKS; st++)
stocks[st].show();
// set pointer to first element
const Stock * top = &stocks[0];
for (st = 1; st < STKS; st++)
top = &top->topval(stocks[st]);
// now top points to the most valuable holding
std::cout << "\nMost valuable holding:\n";
top->show();}
// std::cin.get();
return 0;
}
2. 类的构造函数和析构函数
2.1 类构造函数
由于类的数据通常是私有的,不能像初始化常规变量一样直接对其进行期望值的初始化,故需要构造函数。
构造函数分为默认构造函数和普通构造函数两种。
当用户没有定义任何构造函数时,编译器会自动创建默认构造函数(注意是“任何”,包括默认构造函数和普通构造函数,所以当用户只创建了一个普通构造函数后,就不能再使用Stock fluffy_the_cat
这种声明方式了,因为编译器没有提供默认构造函数),但这个默认构造函数只能创建未初始化的类对象,类似于:
int a
由于编译器自动创建的类构造函数不能对值进行初始化,这样可能会带来一些问题,所以通常我们都会自己定义一个默认构造函数和一个普通构造函数。
2.1.1默认构造函数
默认构造函数即没有任何参数的构造函数(或者是,有参数,但所有参数都必须提供了默认值),是在用户没有提供显式的初值时,用来创建对象的构造函数。用于下面这种声明:
Stock fluffy_the_cat;
下面是一个自己定义的默认构造函数的例子(CPP第十章 P355):
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
2.1.2普通构造函数
有了普通构造函数,用户就能够按自己的期望值对类对象进行初始化。
(在构造函数中,我们可以像以前一样给函数提供默认参数,但是要记得只能在参数列表从后往前提供(即如果给一个参数提供了默认值,那么它右边的所有参数都要有默认值)
以下是一个构造函数的定义:
Stock::Stock(const std::string & co, long n, double pr)
{
company = co;
if (n < 0)
{
std::cout << "Number of shares can't be negative; "
<< company << " shares set to 0.\n";
shares = 0;
}
else
shares = n;
share_val = pr;
set_tot();
}
构造函数可以用以下几种格式来使用:
//格式一
Stock food = Stock("World Cabbage", 250,1.25);
//格式二
Stock food("World Cabbage", 250,1.25);
//使用new创建
Stock *pstock = new Stock("World Cabbage", 250,1.25);
2.2 析构函数
构造函数负责创建对象,当对象过期时,则由析构函数将其删除。
析构函数最需要注意的一点是:当构造函数使用new分配了内存时,一定要在析构函数中使用delete将其删除,否则将造成内存泄漏。
(由于delete
和delete[]
不兼容,所以我们在构造函数中使用new分配内存时,当只有一个元素时,可以使用 str = new char[1]
这种方式,这样就可以在析构函数中统一使用delete[]
)
如果构造函数没有进行内存分配,析构函数可以定义为一个空函数:
Stock::~Stock()
{
}
和构造函数一样,当我们没有定义析构函数时,编译器会自动提供一个析构函数——一个不包含任何内容的空函数。
2.3 复制构造函数
2.3.1 何时调用复制构造函数
每当程序生成了对象副本时,编译器都将调用复制构造函数。具体地说:
1. 创建一个新对象并将其初始化为一个已有对象
2.当函数按值传递对象或函数按值返回对象(而不是引用)时
例:
//情况1
String a("dada");
String b = a;//使用复制构造函数
String c(a);//使用复制构造函数
String d = String(a);//使用复制构造函数
String *p = new String(a);//使用复制构造函数
//情况2(这是一个矩阵乘法中对运算符进行重载的例子)
Matrix operator*(const Matrix& M1, const Matrix& M2)
{
if (M1.cols != M2.row)
{
Matrix M3(0, 0);
M3.p = NULL;
cout << "矩阵A的行数不等于矩阵B的列数,不能相乘!" << endl;
return M3;//返回值之后程序将在调用这个函数的地方用复制构造函数创建一个新的临时对象
}
else
{
Matrix M3(M1.row, M2.cols);
for (int i = 0;i < M1.row;i++)
for (int j = 0;j < M2.cols;j++)
for (int k = 0; k < M1.cols; k++)
M3.p[i*M2.cols + j] += M1.p[i*M1.cols + k] * M2.p[k*M2.cols + j];
return M3;
}
}
2.3.2 复制构造函数的定义
当我们在构造函数中使用new来分配内存时,就必然要在析构函数中使用delete来释放内存,但是这样就会带来一些问题,比如说当我们想用一个对象来初始化另一个对象时,编译器只会将新对象的指针指向原有对象的内存地址,而不会去为他开辟一块新的内存空间,最后就会导致析构函数中的delete将同一块内存delete两次,这个时候就需要我们手动定义一个复制构造函数。
复制构造函数的使用方法通常是:
String a("dada");
String b = a;//使用复制构造函数
通常,复制构造函数与下面这个例子(p447)类似:
String::String(const String & st)
{
num_string++;
len = st.len;
str - new char [len+1];
std::strcpy(str, st.str);
}
2.4 赋值运算符的重载
与2.3中提到的情况类似,当我们想将一个对象赋值给另一个对象时,也会遇到内存分配的问题,因此需要对赋值运算符进行重载来解决这样的问题。
通常,赋值运算符的重载方法与下面这个例子(P447)类似
String & String::operator=(const String & st)
{
if(this == &st)
return *this;
delete [] str;
len = st.len;
str = new char[len + 1];
std::strcpy(str, st.str);
return *this;
}
通常,此方法应该包含以下几种操作:
1)检查自我赋值的情况
2)释放成员指针以前指向的内存
3)复制数据而不仅仅是数据的地址
4)返回一个指向调用对象的引用
2.5 在构造函数中使用new时应该注意的事项
- 如果在构造函数中使用new来初始化指针,则应该在析构函数中使用delete(new对应于delete, new[]对应于delete[],当只有一个元素时,可以使用
str = new char[1]
这种方式,这样就可以在析构函数中统一使用delete[]
) - 在一个构造函数中使用new初始化指针,而在另一个函数中将指针初始化为空,这样的操作是被允许的,因为delete可以用于空指针。
- 应该定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象.
- 应该定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象
3. this指针
在成员函数内部,可以用this
来表示指向当前对象的指针,用*this
来表示当前对象。
例如在下面这种情况中,我们想用topval函数来找出两个对象中总值total_val较大的那一个,并将它的引用作为返回值返回,这个时候就要用this指针来表示当前调用函数的这个对象,用传递进来的参数表示另一个对象。
const Stock & Stock::topval(const Stock & s) const
{
if (s.total_val > total_val)
return s;
else
return *this;
}
此函数具体使用例子参见文章最开始的例子。
4. 关于类中的静态变量
- 类的所有对象共享一个静态成员
- 不能在类声明中初始化静态成员变量,因为声明只是描述了定义该变量的方法,并没有分配内存空间。如果需要,可以在函数外用单独的语句初始化静态变量。以下是一个初始化静态变量的例子(p427 strnbad.cpp):
// strngbad.cpp -- StringBad class methods
#include <cstring> // string.h for some
#include "strngbad.h"
using std::cout;
// initializing static class member
int StringBad::num_strings = 0;
// class methods
// construct StringBad from C string
StringBad::StringBad(const char * s)
{
len = std::strlen(s); // set size
str = new char[len + 1]; // allot storage
std::strcpy(str, s); // initialize pointer
num_strings++; // set object count
cout << num_strings << ": \"" << str
<< "\" object created\n"; // For Your Information
}
StringBad::StringBad() // default constructor
{
len = 4;
str = new char[4];
std::strcpy(str, "C++"); // default string
num_strings++;
cout << num_strings << ": \"" << str
<< "\" default object created\n"; // FYI
}
StringBad::~StringBad() // necessary destructor
{
cout << "\"" << str << "\" object deleted, "; // FYI
--num_strings; // required
cout << num_strings << " left\n"; // FYI
delete [] str; // required
}
std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
os << st.str;
return os;
}
- 如果类中包含这样的静态数据成员,它的值在新对象被创建时发生变化,则应该提供一个显示复制构造函数来处理计数问题。
5. 成员初始化列表
对于类对象中的const数据成员,我们不能用常规的在函数中赋值的方法对其进行赋值,这个时候就需要用到成员初始化列表。一个例子如下:
Queue::Queue(int qs) : qsize(qs)
{
front = rear = NULL;
items = 0;
}
在这个例子中,也可以改写成这样:
Queue::Queue(int qs) : qsize(qs), front(NULL), rear(NULL), items(0)
{
}
成员初始化列表有以下几个需要注意的点:
- 成员初始化列表只能用于构造函数
- 非静态const数据成员和引用数据成员只能用这种方式来初始化
- 数据成员被初始化的顺序与类声明中的顺序相同,与初始化器中的顺序无关。
6. 如何保证成员函数不会修改对象
可以在函数后面加上const,例如:
//函数声明如下(通常在.h文件中)
void stock::show() const;
//函数的具体实现如下(通常在.cpp文件中)
void stock::show() const
{
//这里写函数的具体内容balabala
}