类
无论什么语言进行面向对象程序设计都是从类的设计开始
C++中,一个类就是一种数据结构
从面向对象程序设计的角度来看,在C++中以一个类作为数据类型定义的变量就是对象
在定义类的任何对象之前必须首先给出这个类的声明
定义对象时关键字class是可有可无的
C++对C语言的结构进行了扩展,在C++中使用关键字struct创建的是一个类
面向对象设计的灵魂就是使用private隐藏类实现
我们一般见到的const都是用来修饰变量的,但是如果这种形式:
unsigned getAge() const;
表示该函数不会改变包含这个函数的类中的任何数据成员,成为只读函数,const出现在参数列表与其函数体之间
一个const成员函数仅能调用其他const成员函数,因为const成员不允许直接或间接的改变对象的状态,而调用非const成员函数可能会间接的改变对象的状态
成员选择符(.)、指针操作符(->)
成员选择符用来访问数据成员和成员函数,但对象的使用者只能访问类的公有成员,无论其是数据成员还是函数成员
类的私有成员仅能由类的成员函数访问
在C++中,用关键字class声明的类,其类成员在默认情况下具有类范围性质,在这种情况下,如果没有为类成员指定任何访问限定符,这个成员将作为私有成员处理
class关键字创建的类,类成员默认是私有的
struct关键字创建的类,类成员默认是公有的
有两种途径可以完成类成员函数的定义:
1,成员函数的声明在类声明之中进行,但在类声明之外定义
2,成员函数的定义也在类声明之中进行。这种定义方式为inline,C++关键字inline专门表示内联。内联定义方式同时提供了成员函数的声明和定义
在定义成员函数时要使用域解析符::,因为其他的类也可能使用相对的成员函数名字,甚至一些顶层函数
在类中定义的函数默认的是内联的
而且,通过在进行成员函数声明的时候使用inline关键字,可将原本定义在类声明之外的成员函数强制变成内联函数
类和对象是C++面向对象编程的核心,这种编程模式功能巨大,但处理不当就会影响程序的效率
通常,如果一个对象通过引用方式传到函数中,而函数又不会通过修改对象的数据成员的值改变该对象的状态,那么最好将函数标记为const,可以预防对参数的误写,同时有些编译器还可以对这种情况进行一些优化
Stack.h例子:
#include <iostream>
using namespace std;
class Stack
{
public:
enum{MaxStack = 5};
void init(){top = -1;}
void push(int n)
{
if(isFull())
{
errMsg("Full stack. Can't push.");
return;
}
arr[++top] = n;
}
int pop()
{
if(isEmpty())
{
errMsg("Empty stack.Poping dummy value.");
return dummy_val;
}
return arr[top --];
}
bool isEmpty() {return top < 0; }
bool isFull() {return top >= MaxStack - 1;}
void dump()
{
cout << "Stack contents ,top to bottom:\n";
for(int i = top; i >= 0 ;i --)
cout << '\t' << arr[i] << endl;
}
private:
void errMsg(const char *msg) const
{
cerr << "\n*** Stack operation failure: " << msg << endl;
}
int top;
int arr[MaxStack];
int dummy_val;
};
TimeStamp例子:
#include <iostream>
#include <ctime>
#include <string>
using namespace std;
class TimeStamp
{
public:
void set(long s = 0)
{
if(s <= 0)
stamp = time(0);
else
stamp = s;
}
time_t get() const
{
return stamp;
}
string getAsString() const
{
return extract(0,24);
}
string getYear() const
{
return extract(20,4);
}
string getMonth() const {return extract(4,3);}
string getDay() const {return extract(0,3);}
string getHour() const {return extract(11,2);}
string getMinute() const {return extract(14,2);}
string getSecond() const {return extract(17,2);}
private:
string extract(int offset,int count) const
{
string timeString = ctime(&stamp);
return timeString.substr(offset,count);
}
time_t stamp;
};
瘦包装器:仅仅封装了一些已有的函数,并未增加什么全新的功能的类
构造函数和析构函数比较特殊:在调用它们时不需要显式地提供函数名,编译器会自动调用它们
构造函数不能有返回值
构造函数主要用来对数据成员进行初始化,并负责其他一些在对象创建时需要处理的事务。构造函数对提高类的壮健性有着重要的作用
如果我们不希望调用默认构造函数有两种方法:
1,可以把无参构造函数声明在private范围内
2,提供有参的构造函数,这样系统就不会提供无参的构造函数
多数情况下,编译器为类生成一个共有默认的构造函数,只有下面两种情况例外:
1,一个类显示的声明了任何构造函数,编译器不生成共有的默认构造函数。在这种情况下,如果程序需要一个默认构造函数,由类的设计者提供
2,一个类声明了一个非共有的默认构造函数,编译器不生成共有的默认构造函数
C++程序员常常会将部分构造函数设计为私有成员,将另一部分设计为共有成员,以确保在创建对象时进行 正确的初始化。一个私有构造函数与普通的私有成员函数一样,拥有类范围属性,因而不能在类之外进行调用
构造函数分为:默认构造函数和带参数构造函数
带参数构造函数中有两种比较特殊:拷贝构造函数(copy structructor)和转型构造函数(convert constructor)
拷贝构造函数创建一个新的对象,此对象是另外一个对象的拷贝品,有两种类型:
Person(Person &);
Person(const Person &);
拷贝构造函数可以有多余一个的参数,但是第一个以后的所有的参数都必须是默认值
如果类的设计者不提供拷贝构造函数,编译器会自动生成一个。编译器生成的这个拷贝构造函数版本完成这样的操作:
将源对象所有数据成员的值逐一赋值给目标对象相应的数据成员
通常,如果一个类包含指向动态存储空间指针类型的数据成员,则就应该为这个类设计拷贝构造函数(如果某个类定义了拷贝构造函数,类的设计者通常再为它添加一个赋值操作符重载函数)
要注意问题的本质,任何问题都要一步一步的考虑,不要越范围,不然会出错的,所以本质很重要,看下面一个程序:
拷贝构造函数只是拷贝类里面的值,不是拷贝类里面指针指向的值!!!
这也是默认拷贝构造函数的一个弊端,现在看改正一下后的程序:
通过上述方式,我们自己设计的拷贝构造函数确保源对象和新对象拥有内容相同的不同区域,这一点是编译器生成的拷贝构造函数所办不到的
类的设计者有时希望能通过某种方式来阻止客户进行对象间的拷贝工作,包括一下两种情况:
1,通过传值方式将对象传递给一个函数
2,通过传值方式返回一个对象
由于有时候对象很大,需要禁止对象间拷贝操作
如果拷贝构造函数是私有的,顶层函数和其他类的成员函数就不能通过传值来传递和返回该类的对象,因为这两个操作都需要调用拷贝构造函数
转型构造函数:顾名思义,就是一个构造函数,用来转换类型。但是它本质还是一个构造函数,因此是自动调用。而且是单参数构造函数!
转型构造函数可替换函数重载机制
转型构造函数和C语言中的类型转化很类似,分为隐式的和显示的,隐式的就像类型转化中的自动转换一样
eg:
void f(Person p);
如果有这样一个调用:
string s = "Turandot";
f(s);
此时,如果有转型构造函数Person(const char *n);则编译器会自动调用,把字符串n转化成对象P,然后调用
这种隐式类型转换的确为我们提供了方面,但有时隐式类型转换会导致一些无法预测的错误,而这些错误往往细微得难以察觉。此时,我们宁愿关闭这种因转换类型构造函数的存在而导致的隐式类型转换动作,以确保程序的正确性。C++提供的关键子explicit实现这个功能,加载转型构造函数的前面
explicit带来的好处是:将难以觉察的,后果严重的运行期错误变成了容易改正的编译期错误
初始化列表仅在构造函数中有效,不能用于其他函数。构造函数的初始化列表可以初始化任何数据成员,但const类型数据成员不能 用其他办法进行初始化
数据成员初始化顺序完全取决于它们在类中声明的顺序(不是对应关系,是赋值的先后,对应还是一一对应),与它们在初始化段中出现的次序无关
new和new[]在分配内存空间的同时,还会调用相应的构造函数,malloc和calloc无法做到
当对象被摧毁时,也会调用一个析构函数
对象的摧毁出现在如下两种情况:
1,以某个类作为数据类型的变量超出其作用于范围
2,用delete操作符删除动态分配的对象
~和类名之间可以有空格 ~ C()和~()一样
由于析构函数不带参数,因此不能被重载,这样,每个类只能拥有一个析构函数,没有返回值
看一个例子:
下面看一个例子:
头文件Timestamp.h
#include <iostream>
#include <ctime>
#include <string>
using namespace std;
class TimeStamp
{
public:
void set(long s = 0)
{
if(s <= 0)
stamp = time(0);
else
stamp = s;
}
time_t get() const{ return stamp;}
string getAsString() const {return extract(0,24);}
string getYear() const {return extract(20,4);}
string getMonth() const {return extract(4,3);}
string getDay() const {return extract(0,3);}
string getHour() const {return extract(11,2);}
string getMinute() const {return extract(14,2);}
string getSecond() const {return extract(17,2);}
private:
string extract(int offset,int count) const
{
string timeString = ctime(&stamp);
return timeString.substr(offset,count);
}
time_t stamp;
};
头文件Task.h:
#include "TimeStamp.h"
#include <iostream>
#include <ctime>
#include <fstream>
#include <string>
using namespace std;
class Task
{
public:
Task(const string ID)
{
setID(ID);
logFile = "log.dat";
setST();
ft = st;
}
Task(const char *ID)
{
setID(ID);
logFile = "log.dat";
setST();
ft = st;
};
~Task()
{
logToFile();
}
void setST(time_t ST=0){st.set(ST);}
time_t getST() const {return st.get();}
string getStrST() const {return st.getAsString();}
void setFT(time_t FT=0) {ft.set(FT);}
time_t getFT() const {return ft.get();}
string getStrFT() const {return ft.getAsString();}
void setID(const string &ID) {id = ID;}
void setID(const char *ID) { id = ID;}
string getID() const {return id;}
double getDU() const {return difftime(getFT(),getST());}
void logToFile()
{
if(getFT() == getST())
setFT();
ofstream outfile(logFile.c_str(),ios::app);
outfile << "\nID: " << id << endl;
outfile << " ST: " << getStrST();
outfile << " FT: " << getStrFT();
outfile << " DU: " << getDU();
outfile << endl;
outfile.close();
}
private:
Task(); //因为任何Task在创建时都应该拥有一个唯一的Task ID,因此不允许调用默认构造函数,为了强调这一点,把默认构造函数定义为私有
//由于定义了其他函数,编译器不会生成一个共有的默认构造函数,设计一个私有的默认构造函数目的完全是为了强调创建Task对象时必须提供一个string或char *字符
//串类型的Task ID
TimeStamp st; //Task起始时间
TimeStamp ft; //Task结束时间
string id; //Task ID
string logFile; //Task信息的日志文件的文件名
};
主文件Task.cpp:
#include "Task.h"
int main()
{
time_t now = time(0);
Task t1("Defrost pizzas"),t2("Open beer"),t3("Eat pizzas and drink beer");
t1.setST(now);
t1.setFT(now+3600);
t2.setST(t1.getFT());
t2.setFT(t2.getST() + 2);
t3.setST(t2.getFT() + 1);
t3.setFT(t3.getST() + 7200);
return 0;
}
输出文件log.dat:
ID: Eat pizzas and drink beer
ST: Fri Sep 28 17:18:26 2012 FT: Fri Sep 28 19:18:26 2012 DU: 7200
ID: Open beer
ST: Fri Sep 28 17:18:23 2012 FT: Fri Sep 28 17:18:25 2012 DU: 2
ID: Defrost pizzas
ST: Fri Sep 28 16:18:23 2012 FT: Fri Sep 28 17:18:23 2012 DU: 3600
Task类的输出到文件的函数在析构函数中调用,是确保任何任务的信息都输出到日志文件中
C++还支持另外一种类型成员,这种成员属于类本身,而不属于类的对象,称为类成员,属于对象的成员称为对象成员或实例成员。我们的static关键字可以胜任这个职责
在类声明内部声明的static数据成员必须在任何程序块之外定义,定义为(赋值)为0,不是必须的,因为在所有程序块之外定义的任何变量都将自动地初始化为0,除非编程人员提供一个不同的值
static数据成员不会影响该类及其对象的sizeof。
class C1
{
unsigned long dm1;
double dm2;
};
class C2
{
unsigned long dm1;
double dm2;
static unsigned long dm3;
static double dm4;
};
sizeof(class C1) == sizeof(class C2) == 16 (windows下)
这是编译器的差异,但是我们已经看到了这个试试,静态变量不影响类的大小
除了static数据成员,类还有static成员函数。
静态成员函数只能访问其他static成员(包括数据和函数)
非静态成员也可以访问static的成员(数据和函数)
但是静态成员函数不能访问非静态的成员(数据和函数)
static可以是inline函数,也可以是非inline函数
static成员可以通过对象来访问,也可以通过类来访问
c.function()(对象访问) 或者C::function()(类访问)
C++中,指向对象的指针主要用于两方面:
其一,指向对象的指针可以作为参数传递给函数,或通过函数返回
其二,使用new和new[]动态创建对象,然后返回一个指向该对象的指针
this指针是一个常量,它不能赋值、递增、递减等运算的目标对象。此外this只在非static成员函数中才有效
常见错误总结:
1,类声明后面的‘;’最容易忘记
2,类的声明必须在定义该类的对象之前
3,只能在成员函数或者friend函数中访问类的非公有成员
4,如果要在类声明之外定义inline函数,关键字inline只能出现在该函数的声明中
5,类成员或者非类成员 只有 静态变量被默认初始化
6,const成员必须在初始化段(列表)中被初始化
7,看下面片段:
看修改后的:
看看有什么区别!
8,指针this是一个常量,因此this作为赋值、递增、递减、操作符的目的对象都是错误的
9,在static成员函数中使用this是错误的
10,在const成员函数中错误的通过赋值操作或其他方法来改变数据成员
11,在const成员函数中调用非const成员是错误的
12,如果函数f有一个对象参数obj标记为const,则在f中调用对象obj的任何非const成员函数都是错误的,因为这些成员函数可能会改变obj的状态