成员变量
每个类可以没有成员,也可以定义多个成员,成员可以是数据、函数或类型别名。
一个类可以包含若干公有的、私有的和受保护的部分。在 public 部分定义的成员可被使用该类型的所有代码访问;在 private 部分定义的成员可被其他类成员访问。protected可以被子类访问。
构造函数
与类同名,且没有返回值的函数,用于构造一个对象。
一般使用一个初始化列表来初始化数据成员,如下:
Sales_item() : units_sold(0),revenue(0.0){}
成员函数
在类内部,声明成员函数是必需的,而定义成员函数则是可选的。
在类内部定义的函数默认为 inline。
在类外部定义的成员函数必须指明它们是在类的作用域中。Sales_item::avg_price 的定义使用作用域操作符来指明这是Sales_item 类中 avg_price 函数的定义。
将const加加到形参表之后,就可以变为常成员函数,在该函数内不能修改成员变量的值,如: double avg_price() const;
const必须出现在声明和定义中,若出现一处,就会出现一个编译时错误。
ps:const成员函数可以区分重载函数
Ø 显示指定inline成员函数
可以在类定义体内部指定一个成员为 inline,作为其声明的一部分。或者,也可以在类定义外部的函数定义上指定 inline。在声明和定义处指定 inline都是合法的。在类的外部定义 inline 的一个好处是可以使得类比较容易阅读。像其他 inline 一样,inline 成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的同一头文件中。
Ø 定义一个类
class Test{
}; //注意分号不能少
声明一个类
class Test; //叫前向声明
Ø 因为只有当类定义体完成后才能定义类,因此类不能具有自身类型的数据成员。然而,只要类名一出现就可以认为该类已声明。因此,类的数据成员可以是指向自身类型的指针或引用,如下:
class LinkScreen {
Screen window;
LinkScreen *next;
LinkScreen *prev;
};
Ø 类的定义分号结束。分号是必需的,因为在类定义之后可以接一个对象定义列表。定义必须以分号结束:
class Sales_item{ /* ... */ };
class Sales_item{ /* ... */ } accum, trans;
Ø 隐含的this指针
成员函数具有一个附加的隐含形参,即指向该类对象的一个指针。这个隐含形参命名为 this,与调用成员函数的对象绑定在一起。成员函数不能定义this 形参,而是由编译器隐含地定义。成员函数的函数体可以显式使用 this 指针,但不是必须这么做(比如return *this;)。如果对类成员的引用没有限定,编译器会将这种引用处理成通过 this 指针的引用。
Ø const 成员函数返回*this
在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向 const 类类型对象的const 指针。 既不能改变 this 所指向的对象, 也不能改变 this 所保存的地址。
****不能从 const 成员函数返回指向类对象的普通引用。const 成员函数只能返回 *this作为一个 const 引用。****
Ø 基于const的重载
基于成员函数是否为 const, 可以重载一个成员函数;同样地,基于一个指针或者引用形参是否指向 const,可以重载一个函数。const对象只能使用 const 成员。非 const 对象可以使用任一成员,但非 const 版本是一个更好的匹配。
Ø 可变数据成员
可变数据成员(mutable data member)永远都不能为 const(冲突),甚至当它是const 对象的成员时也如此。因此,const 成员函数可以改变mutable 成员。要将数据成员声明为可变的,必须将关键字 mutable 放在成员声明之前:
mutableint num;
Ø 形参表和函数体处于类作用域中
在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后。这些都是在类作用域中定义,所以可以不用限定而引用其他成员。
charScreen::get(index r, indexc) const
{
index row = r * width; // compute the row location
return contents[row + c]; // offset by c to fetch specifiedcharacter
}
该函数用 Screen 内定义的 index 类型来指定其形参类型。因为形参表是在 Screen 类的作用域内,所以不必指明我们想要的是 Screen::index。我们想要的是定义在当前类作用域中的,这是隐含的。同样,使用 index、width 和contents 时指的都是 Screen 类中声明的名字。
Ø 函数返回类型不一定在类作用域中
与形参类型相比,返回类型出现在成员名字前面。如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外。如果返回类型使用由类定义的类型,则必须使用完全限定名。例如,考虑 get_cursor 函数:
classScreen {
public:
typedef std::string::size_type index;
index get_cursor() const;
};
inline Screen::index Screen::get_cursor() const
{
return cursor;
}
Ø 构造函数
Sales_items(); 对象在栈区
Sales_item*p=new Sales_item(); 对象在堆区
ps:构造函数不能声明为const。
Sales_item()const; //error
pps:与其他函数相同的是,构造函数具有名字(与类名相同),形参表,函数体,不同的是无返回值,返回类型,且包含一个构造函数初始化列表:
Sales_item::Sales_item(conststring &book):isbn(book), units_sold(0), revenue(0.0) { }
等价于
Sales_item::Sales_item(conststring &book)
{
isbn = book;
units_sold = 0;
revenue = 0.0;
}
注意:这个构造函数给类 Sales_item 的成员赋值,但没有进行显式初始化。不管是否有显式的初始化式,在执行构造函数之前,要初始化 isbn 成员。这个构造函数隐式使用默认的 string 构造函数来初始化 isbn。执行构造函数的函数体时,isbn成员已经有值了。该值被构造函数函数体中的赋值所覆盖。
从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化。运行该类型的默认构造函数,来初始化类类型的数据成员。内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为 0。
ppps:有时需要构造函数初始化列表。如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。在这种情况下,为了初始化数据成员,必须提供初始化式。有些成员必须在构造函数初始化列表中进行初始化。对于这样的成员,在构造函数函数体中对它们赋值不起作用。没有默认构造函数的类类型的成员,以及 const 或引用类型的成员,不管是哪种类型,都必须在构造函数初始化列表中进行初始化。
如:
classConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
ConstRef::ConstRef(intii)
{ i =ii; // ok
ci = ii;// error: cannot assign to a const,只能初始化
ri = i; // assigns to ri which was not bound to an object,errpr
}
改为:ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
ps:成员初始化的顺序
每个成员在构造函数初始化列表中只能指定一次,这不会令人惊讶。毕竟,给一个成员两个初始值意味着什么?也许更令人惊讶的是,构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。 第一个成员首先被初始化,然后是第二个, 依次类推。初始化的次序常常无关紧要。然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的。
如:
class X{
int i;
int j;
public:
// run-time error: i is initialized before j
X(int val): j(val), i(j) { }
};
在这种情况下,构造函数初始化列表看起来似乎是用 val 初始化 j,然后再用 j 来初始化 i。然而,i 首先被初始化。但这个初始化列表的实际效果是用尚未初始化的 j 值来初始化 i!
如果数据成员在构造函数初始化列表中的列出次序与成员被声明的次序不同,那么有的编译器非常友好,会给出一个警告。按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
写成X(int val): i(val), j(val) { }可能更好!
l 初始化式可以是任意表达式,Sales_item(conststd::string &book, int cnt, double price) : isbn(book), units_sold(cnt),revenue(cnt * price) { }
l 类类型的数据成员的初始化式,可以使用该类型的任意构造函数。例如,Sales_item 类可以使用任意一个 string构造函数来初始化 isbn。也可以用 ISBN 取值的极限值来表示isbn 的默认值,而不是用空字符串。即可以将 isbn 初始化为由 10 个 9 构成的串:
Sales_item(): isbn(10, '9'),units_sold(0), revenue(0.0) {}
l 构造函数也可以有默认参数
l 默认构造函数
只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数。哪怕只定义了一个构造函数,编译器也不会再自动生成默认构造函数了。合成的默认构造函数使用与变量初始化相同的规则来初始化成员。即类成员使用默认构造函数,内置变量不初始化(与java不同,java中都是值初始化,即类成员为null,int为0,double为0.0)。
ps:通常应为类定义一个默认构造函数,如果没有,则会有以下问题:
1、定义对象时必须传参
2、该类不能用作动态分配数组的元素类型
3、如果容器保存该类对象,如vector,则不能使用只接受容器大小的构造函数
pps:使用默认构造函数
Sales_item myobj; //正确!
或者 Sales_item myobj=Sales_item(); //先创建一个无名对象,再用它复制给myobj
Sales_item myobj(); //当成了一个函数声明!!
Ø 隐式类类型转换
classSales_item {
public:
Sales_item(const std::string &book = ""):isbn(book),units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is);
};
当string null_book = "9-999-99999-9";
item.same_isbn(null_book);
和item.same_isbn(cin); 都会隐式执行构造函数创建一个临时对象,再调用函数same_isbn。
但是如下:
classSales_item {
public:
explicit Sales_item(const std::string&book = ""):isbn(book), units_sold(0), revenue(0.0) { }
explicit Sales_item(std::istream&is);
};
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不可再写它!!
item.same_isbn(null_book); // error: string constructor is explicit
item.same_isbn(cin); // error:istream constructor is explicit
只能item.same_isbn(Sales_item(null_book));
ps:通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数(转换构造函数??)应该为 explicit。将构造函数设置为explicit可以避免错误,并且当转换有用时,用户可以显式地构造对象。
Ø 当类的全体数据成员都是 public时,可以Data val2 = { 1024, "Anna Livia Plurabelle" };
这样初始化,且列表顺序为成员变量定义的顺序。
Ø 友元
友元机制允许一个类将对其非公有成员的访问权授予指定的函数或类。友元的声明以关键字friend 开始。它只能出现在类定义的内部。友元声明可以出现在类中的任何地方:友元不是授予友元关系的那个类的成员,所以它们不受声明出现部分的访问控制影响。通常, 将友元声明成组地放在类定义的开始或结尾是个好主意。
ps:在类内部public下的typedef 的类型名,在类外访问要用类名::类型名,如vector<string>::iterator
例子:
classScreen {
friend class Window_Mgr; //友元类,则Window_Mgr 的所有成员函数可以直接引用 Screen 的私有成员。
};
ps:友元可以是普通的非成员函数,或前面定义的其他类的成员函数,或整个类。将一个类设为友元,友元类的所有成员函数都可以访问授予友元关系的那个类的非公有成员。
classScreen {
friendWindow_Mgr&Window_Mgr::relocate(Window_Mgr::index,Window_Mgr::index,Screen&); //友元成员函数
};
ps:当我们将成员函数声明为友元时,函数名必须用该函数所属的类名字加以限定。
² 友元声明与作用域
为了正确地构造类,需要注意友元声明与友元定义之间的互相依赖。在前面的例子中,类 Window_Mgr 必须先定义。否则,Screen 类就不能将一个Window_Mgr 函数指定为友元。然而,只有在定义类 Screen 之后,才能定义relocate 函数——毕竟,它被设为友元是为了访问类 Screen 的成员。
Ø static类成员
类可以定义共享的 static 数据成员,类也可以定义 static 成员函数。static 成员函数没有 this 形参,它可以直接访问所属类的 static 成员,但不能直接使用非 static 成员。
ps:可以通过作用域操作符从类直接调用 static 成员,或者通过对象、引用或指向该类类型对象的指针间接调用。如:
classAccount {
public:
void applyint() { amount += amount * interestRate; }
static double rate() {return interestRate; }
static void rate(double);// sets a new rate
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
Accountac1;
Account*ac2 = &ac1;
rate =ac1.rate(); // through an Account object or reference
rate =ac2->rate(); // through a pointer to anAccount object
rate =Account::rate(); // directly from the class using thescope operator
ps:当我们在类的外部定义 static 成员时,无须重复指定 static 保留字,该保留字只出现在类定义体内部的声明处,同explicit。
pps:因为 static 成员不是任何对象的组成部分,所以 static 成员函数不能被声明为 const。毕竟,将成员函数声明为const 就是承诺不会修改调用该函数的对象,而static成员函数本质上是类名换用(即使用对象调用了!)。 最后, static 成员函数也不能被声明为虚函数(不被重写!!)。
ppps:static 数据成员必须在类定义体的外部定义(正好一次)。不像普通数据成员,static成员不是通过类构造函数进行初始化,而是应该在定义时进行初始化。
double Account::interestRate = initRate();
同static成员函数,static 关键字只能用于类定义体内部的声明中,定义不能标示为static。
² 特殊的整型 const static 成员,可以在类的定义体中进行初始化。
classAccount {
public:
static double rate() { return interestRate; }
static void rate(double); // sets a new rate
private:
static const int period = 30; // interest postedevery 30 days
double daily_tbl[period]; // ok: period is constant expression
};
constint Account::period; //没有static,没有初始值
ps:const static 数据成员在类的定义体中初始化时,该数据成员仍必须在类的定义体之外进行定义。在类内部提供初始化式时,成员的定义不必再指定初始值,如上。