函数模板
tmplate <typename Anytype>:建立一个模板,并将类型命名为AnyType。关键字template和typename是必需的,除非可用class代替typename。
template <typename AnyType>
void Swap(AnyType &a, Anytype &b)
{
AnyType temp;
temp = a;
a = b;
b = temp;
}
如果需要多个将同一种算法用于不同类型的函数,使用模板。
局限性:
假设定义如下结构:
sturct job
{
char name[40];
double salary;
int floor;
};
假设只想交换salary和floor成员,而不交换name成员,上述模板不能实现。此时需要模板具体化,命令编译器创建特定的实例。
普通原型:void Swap(job &, job &);
模板原型:template <typename T>
void Swap(T &, T &);
显示具体化:template <> void Swap<job>(job &, job &);
如果有多个原型,编译器在选择原型时,非模板 优先于 现实具体化 优先于 模板版本。
单独编译:
分为三部分:
1)头文件:包含结构声明和使用和谐结构的函数的原型
2)源代码文件:包含与结构有关的函数的代码
3)源代码文件:包含调用与结构相关的函数的代码
不要将函数的定义或变量声明放到头文件中。例如,如果在头文件中包含一个函数定义,然后在其他两个文件(属于同一个程序)中包含该头文件,则同一个程序中将包含同一个函数的两个定义,除非函数是内联的,否则将出错。
头文件中长包含的内容:
1)函数原型
2)使用#define或const定义的符号常量
3)结构声明
4)类声明
5)模板声明
6)内联函数
在同一个文件中只能将同一个头文件包含一次。
#ifndef COORDIN_H
//基于预处理编译指令#ifndef(if not defined)
...
/*代码片段意味着仅当以前没有使用预处理器编译指令#define 定义名称COORDIN_H_时,才处理#ifndef和#endif之间的语句*/
#endif
例:
//coordin.h
#ifndef COORDIN_H_
/*
编译器首次遇到该文件时,名称COORDIN_H_没有定义。这种情况下,编译器将查看#ifndef和#endif之间的内容(这正是我们希望的)。
如果在同一个文件中遇到其他包含coordin.h的代码,编译器将知道COORDIN_H_已经被定义了,从而调到#endif后面的一行上。
*/
#define COORDIN_H_
struct polar
{
double distance;
double angle;
};
struct rect
{
double x;
double y;
};
polar rect_to_polar(rect xyops);
void show_polar(polar dapos);
#endif
OOP 对象和类
OOP特性:
1)抽象
2)封装和数据隐藏
3)多态
4)继承
5)代码可重用性
通常将类定义放在头文件中,类方法的代码放在源代码文件中。
定义位于类生命中的函数都将自动成为内联函数。
类的构造函数和析构函数
程序只能通过成员函数来访问数据成员,因此需要设计合适的成员函数,才能成功地将对象初始化。
类构造函数专门用于构造新对象,将值赋给数据成员。
Stock::stock(const string & co, long n, double pr)
{
company = co;
if( n < 0 )
{
std::cerr<<"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 grament("Furry Mason", 50, 2.5);
将构造函数与new一起使用:Stock *pstock = new Stock("Electroshock Games", 18, 19.0); 创建一个Stock对象,将其初始化为参数提供的值,并将该对象的地址赋给pstock指针。这种情况下,对象没有名称,但可以使用指针来管理该对象。
构造函数被用来创建对象,而不能通过对象来调用。
默认构造函数:如果没有提供任何构造函数,则C++将自动提供默认构造函数,它是默认构造函数的隐式版本,不做任何工作。
默认函数是在未提供显示初始值时,用来创建对象的构造函数。 用于如下声明: Stock fluffy_the_cat; // 使用默认构造函数
默认构造函数可能如下:Stock::stock(){ }
定义默认构造函数的两种方式。
1)给已有构造函数的所有参数提供默认值:Stock(const string * co = "Error", int n = 0, double pr = 0.0);
2)通过函数重载定义另一个构造函数——一个没有参数的构造函数:Stock();
在设计类时,通常提供对所有类成员做隐式初始化的默认构造函数。
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
构造函数还可如下:
/*
TableTennisPlayer类记录会员的姓名以及是否有球桌。
*/
TableTennisPlayer::TableTennisPlayer (const string & fn,
const string & ln, bool ht) : firstname(fn),
lastname(ln), hasTable(ht) {}
/*
与下面这样写效果一样
*/
TableTennisPlayer::TableTennisPlayer(const string & fn,
const string & ln, bool ht)
{
firstname = fn;
lastname = ln;
hasTable = ht;
}
隐式的调用默认构造函数时,不要使用圆括号。
Stock first("concret conglomerate"); //调用构造函数
Stock second(); //声明一个方法,second是一个返回Stock对象的函数
Stock third; //调用默认构造函数
调用一个默认构造函数后,Stock stock;
使用显式构造函数 stock = Stock("Nifty Foods", 10, 50.0)对其成员赋值
析构函数:
对象过期时,程序将自动调用一个特殊的成员函数——析构函数。如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。
析构函数名称:在类名前面加上~
析构函数没有参数。Stock的析构函数原型:
Stock::~Stock()
{
}
如果程序员没有提供析构函数,编译器将隐式的声明一个默认的析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。
//stock10.h
#ifndef STOCK10_H_
#define STOCK!)_H_
#include <string>
class Stock
{
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot(){total_val = shares * share_val ; }
public:
Stock(); //默认构造函数
Stock(const std::string & co, long n = 0, double pr = 0.0);
~Stock();//析构函数
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
#endif
保证函数不会修改调用对象:C++解决方法是将const关键字放在函数的括号后面。
声明:void show() const;
定义:void sotck::show() const{}
C++11允许将列表初始化语法用于类,只要提供与某个构造函数的参数列表匹配的内容,并用大括号将他们括起。
Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0};
Stock jock {"Sport Age Storage, Inc"};
Stock temp();
this指针
this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)。
每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。
对象数组
Stock mystuff[4];
当程序创建未被显式初始化的类对象时,总是调用默认构造函数。
运算符重载
格式:operatorop(argument-list)
例如:operator+():重载+运算符;operator*():重载*运算符
//mytime1.h -- Time class before operator overloading
#ifndef MYTIME1_H_
#define MYTIME1_H_
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0, int m = 0);
Time operator+(const Time & t) const;
void show() const;
};
#endif
//mytime1.cpp -- implementing Time methods
#include <iostream>
#include "mytime1.h"
...
Time Tiem::operator+(const Time & t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
...
友元
通常,公有类方法提供唯一的对类私有部分访问途径,但C++提供了另一种形式的访问权限:友元。
友元有三种:
1)友元函数
2)友元类
3)友元成员函数
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
创建友元:创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字friend
friend Time operator*(double m, const Time & t);
该原型意味着:1.虽然operator*()函数是在类声明中声明的,但他不是成员函数,因此不能使用成员运算符来调用;2.虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同。
第二步:编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend
定义如下:
Time operator*(double m, const Time & t)
{
Time result;
long totalminutes = t.hours * m *60 + t.minutes * m;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
类继承
能够从已有的类派生出新的类,而派生类继承了原有类(基类)的特征,包括方法。
可通过继承完成的工作:
1)可以在已有的类上添加新功能
2)可以给类添加数据
3)可以修改类的方法
一个简单的基类:
class TableTennisPlayer
{
private:
string firstname;
string lastname;
bool hasTable;
public:
TableTennisPlayer(const string & fn = "none", const string & ln = "none", bool ht = false);
void Name() const;
bool HasTable() const {return hasTable;}
void ResetTable(bool v){ hasTable = v;}
};
派生一个类:
class RatedPlayer : public TableTennisPlayer
{
...
};
冒号支出RatedPlayer类的基类是TableTennisPlayer类。上述特殊的声明头表明TableTennisPlayer是一个公有基类,这称为公有派生。
使用公有派生,基类的公有成员将称为派生类的公有成员;基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。
派生类需要自己的构造函数;派生类可以根据需要添加额外的数据成员和成员函数
下面例子,派生类需要另一个数据成员来存储比分,还应包含检索比分的方法和重置比分的方法。因此,类声明如下:
class RatedPlayer : public TableTennisPlayer
{
private:
unsigned int rating;
public:
RatedPlayer (unsigned int r = 0, const string & fn = "none", const string & ln = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer & tp);
unsigned int Rating() const { return rating; }
void ResetRating ( unsigned int r) { rating = r; }
};
构造函数必须给新成员(如果有的话)和继承的成员提供数据。在第一个RatedPlayer构造函数中,每个成员对应一个形参;第二个RatedPlayer构造函数使用一个TableTennisPlayer参数,该参数包括firstname, lastname, hasTable
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问。派生类构造函数必须使用基类构造函数。
创建派生类对象时,程序首先创建基类对象。下面是第一个RatedPlayer构造函数代码:
RatedPlayer::RatedPlayer(unsigned int r, const string & fn,
const string & ln, bool ht) : TableTennisPlayer(fn, ln, ht)
{
rating = r;
}
派生类和基类的关系:1.派生类对象可以使用基类的方法,条件是方法不是私有的。2.基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象。3.基类指针或引用只能用于调用基类方法。
派生类不能直接访问基类的私有成员,必须使用基类的公有方法才能访问这些数据。访问的方式取决于方法。构造函数使用一种技术,而其他成员函数使用另一种技术。
构造函数:
//第一种方法
BrassPlus::BrassPlus( const string & s, long an, double bal,
double ml, double r ) : Brass(s, an ,bal)
{
maxLoan = ml;
oweBank = 0.0;
rate = r;
}
//第二种方法
BrassPlus::BrassPlus(const Brass & ba, double ml, double r)
: Brass(ba) // use implicit copy constructor
{
maxLoan = ml;
oweBank = 0.0;
rate = r;
}
继承:is-a关系(公有继承)
3种继承方式:公有继承、保护继承和私有继承
公有继承:is-a关系,即派生类对象也是一个基类对象,可以对基类对象执行的任何操作,也可以对派生类对象执行。继承可以在积累的基础上添加属性,但不能删除积累的属性。
is-a关系:例如:假设有一个Fruit类,可以保存水果的重量和热量。因为香蕉是一种特殊的水果,所以可以从Fruit类派生出Banana类。
多态公有继承:
多态:希望同一个方法在派生类和基类中的行为是不同的。方法的行为应取决于调用该方法的对象。
两种重要的机制可用于实现多态公有继承:
1)在派生类中重新定义基类方法
2)使用虚方法
使用virtual:如果方法通过引用或指针而不是对象调用,它将确定使用哪一种方法。如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了virtual,程序将根据引用或指针只想的对象的类型来选择方法。virtual只用于类声明的方法原型中。不用于方法定义中。
经常在基类中将派生类会重新定义的方法生命为虚方法。为基类声明一个虚析构函数也是一种惯例。这样做是为了确保释放派生对象时,按正确的顺序调用析构函数。
访问控制:protected
protect与private相似,在类外只能用公有类成员来访问protected部分中的类成员。protected和private之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似。