1.inline成员函数的定义必须在调用该函数的每个源文件中是可见的。不在类定义体内定义的inline成员函数,其定义通常应放在有类定义的头文件中。
2.一旦定义了类,那么我们就知道了所有类成员,以及存储该类的对象所需的存储空间。
3.在创建类的对象之前,必须完整地定义该类,而不只是声明类。这样,编译器就会给类的对象预定相应的存储空间。同样,在使用引用或指针访问类的成员之前,必须定义类。
4.只有当类定义已经在前面出现过,数据成员才能被指定为该类类型。如果该类型是不完全类型,那么数据成员是指向该类类型的指针或引用。类的前向声明一般用来编写相互依赖的类。如,两类之间的交叉定义以及在类定义内部需要定义该类的指针或引用。
class LinkScreen{
Screen window;
LinkScreen *prev;
LinkScreen *next;
}
5.(一般而言)定义类时不进行存储分配;定义对象时,将为其分配存储空间。
6.每个对象具有自己的类数据成员的副本(若类定义中含有static类型数据时除外,因为静态数据成员和静态成员函数是为类而非对象服务的,所以对象不具有静态数据的副本)
7.我们一般定义类如下面第一行所示:
当然第二行定义也是合法的(不过不建议这么做),因为它在定义类的同时,也定义了对象。这样做,会使得所发生的操作难以理解。对读者而言,将2个不同实体(类和变量)组合在一个语句中,会让人疑惑不解。
class Sales_item{/* ... */};
class Sales_item{/* ... */}accum,trans;
8.必须显示使用this指针的情况:当我们需要将一个对象作为一个整体引用而不是引用对象的一个成员时。最常见的情况是函数返回对调用该函数的对象的引用。示例:
#
我们定义两个操作:move和set
set:将特定字符或光标指向的字符设置为定值;
move:给定两个index的值,将光标移至新位置。
理想情况下,希望程序员能够将这些操作的序列连接成一个单独的表达式:
myScreen.move(4.0).set('#');
//上语句等价于
myScreen.move(4.0);
myScreen.set('#');
而要能连成一句的条件就是myScreen.move(4.0)
返回的是对象整体。
在这两个操作中,必须要返回一个引用,该引用指向指向操作的那个对象。
#
//Screen.h
class Screen{
public:
//interface member functions
Screen& move(index r,index c);
Screen& set(char);
Screen& set(index,index,char);
// other member as before
}
//Screen.cpp
Screen& Screen::set(char c)
{
contents[cursor] = c;
return *this;
}
Screen& Screen::move(index r,index c)
{
index row = r*width;
cursor = row + c;
return *this;
}
9.从const成员函数返回*this,该类型的this是一个指向const类类型对象的const指针。既不能改变this所指向的对象值,也不能改变this所保存的地址。(c++ primer 377 中举出来关于display函数的例子相当有意思,之后可以好好琢磨)
10.成员函数重载和普通函数应用相同的规则:两个重载成员的形参数量和类型不能完全相同。
这里,关于重载,需要考虑一种现象:const重载
//首先分析const修饰成员函数和返回值的情况:
class A
{
public:
void f(int i){ std::cout<<"1";}; //函数1
void f(int i) const { std::cout<<"2" ;};//函数2
};
//这个地方的重载是没有问题的,说明const 修饰函数能够区分重载
class B
{
void f(int i);
const void f(int i);
};
//这次编译器会报出 'B::f' : redefinition; 的错误,说明const作为修饰返回值不能够区分重载
//再者分析const修饰值传递和指针(引用)传递的情况:
class C
{
void f(int i);
void f(const int i);
};
//这个是错误的,编译通不过。那么是不是说明内部参数的const不予重载呢?
class D
{
public:
void f(int &i) { std::cout<<"3";}; //函数3;
void f(const int &i){ std::cout<<"4" ;};//函数4
};
// 这个程序是正确的,看来上面的结论是错误的。
//分析:为什么会这样呢?这要涉及到接口的透明度问题。按值传递时,对用户
//而言,这是透明的,用户不知道函数对形参做了什么手脚,在这种情况下进行
//重载是没有意义的,所以规定不能重载!当指针或引用被引入时,用户就会对
//函数的操作有了一定的了解,不再是透明的了,这时重载是有意义的,所以规
//定可以重载。
//这里要注意的一点:返回值不能作为区分重载的条件。
11.全局作用域符::
如果我们需要使用某个全局对象,但因为类中存在同名变量,将全局变量屏蔽了。我们可以采用全局作用域符来限定名字。
如:
int height;
class Screen
{
public:
void dummy_fcn(index height)
{
cursor = width * ::height; //::限定height为全局变量
}
private:
index height;
index width;
}
12.构造函数初始化式:有些成员必须在构造函数初始列表中进行初始化。对于这样的成员,在构造函数体重对它们赋值不起作用。这样的成员指的是没有默认构造函数的类类型成员,以及const或引用类型的成员。
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
// no explicit constructor initializer: error ri is uninitialized
ConstRef::ConstRef(int ii)
{ // assignments:
i = ii; // ok
ci = ii; // error: cannot assign to a const
ri = i; // assigns to ri which was not bound to an object
}
初始化const或引用类型数据成员的唯一机会是在构造函数初始化列表中。
// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }
13.构造函数初始化类别仅指定用于初始化成员的值,并不知道这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。
class X {
int i;
int j;
public:
// run-time error: i is initialized before j
X(int val): j(val), i(j) { }
};
上述代码段中,由于i的初始化次序早于j,所以用j来初始化i是非法的。
按照与成员声明一致的次序编写构造函数初始化列表是个好主意。此外,尽可能避免使用成员来初始化其他成员。
14.使用默认实参,可以减少代码重复。(详情见C++ Primer Page 391)
15.由于指针和数组,只对定义在全局作用域中的对象才初始化,当对象定义在局部作用域中时,内置或符合类型的成员不进行初始化。所以如果类包含内置或符合类型的成员,则该类不应该依赖于默认构造函数。它应该定义自己的构造函数来初始化这些成员。
16.类通常应定义一个默认构造函数。且通常,在默认构造函数在给成员提供的初始值应该指出该对象是“空”的。
17.通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit。将构造函数设置为explicit可以避免错误,并且当转换有用时,用户可以显式构造对象。(详情见C++ Primer Page394隐式类类型转换,这部分较为抽象,需要反复研读)
18.定义和使用构造函数几乎总是好的。当我们为自己定义的类型提供一个默认构造函数时,允许编译器自动允许那个构造函数,以保证每个类对象在初次使用之前正确地初始化。
19.友元声明和友元定义之间相互依赖:
class Screen {
// Window_Mgr members can access private parts of class Screen
friend class Window_Mgr;
// ...restofthe Screen class
};
Window_Mgr&
Window_Mgr::relocate(Screen::index r, Screen::index c,
Screen& s)
{
// ok to refer to height and width
s.height += r;
s.width += c;
return *this;
}
在这个例子中,类Window_Mgr必须先定义。否则,Screen类就不能将Window_Mgr函数指定为友元。然而relocate函数中由于使用了类Screen而且访问了类Screen成员,所以函数relocate的定义要在类Screen定义之后。
20.每个static数据成员是与类关联的,并不与该类的对象关联。
21.static成员是类的组成部分但不是任何对象的组成部分,因此,static成员函数没有this指针。因为static成员不是任何对象的组成部分,所以static**成员函数**不能被声明为const(但是static数据成员可以定义为const)。毕竟,将成员函数声明为const就是承诺不会修改该函数所属的对象。最后,static成员函数不能声明为虚函数。
22.static数据成员的类型可以是该成员所属的类类型。非static成员被限定声明为其自身类对象的指针或引用:
class Bar {
public:
// ...
private:
static Bar mem1; // ok
Bar *mem2; // ok
Bar mem3; // error
};
23.一些知识点的总结:
(1)空类的大小为1字节(为什么不是0字节?主要是因为每个对象在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含地加一个字节,这样空类在实例化后在内存能够得到了独一无二的地址,所以空类所占的内存大小是1个字节)。这里还要注意的是如果类不为空,那么编译器不会再隐含地添加这一字节。
(2)类内部的成员变量:
普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
static变量:不占用内存,原因是编译器将其放在全局变量区。
类内部的成员函数:
普通函数:不占用内存。
虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的。
24.同一个类的多个对象共享函数代码。而我们访问类的成员函数是通过类里面的一个指针实现,而这个指针指向的是一个table,table里面记录的各个成员函数的地址。(可参考《c++类的成员函数存储方式(是否属于类的对象)—一道面试题引发的思考》http://blog.csdn.net/richerg85/article/details/19540437和《C++中的类所占内存空间总结》http://blog.sina.com.cn/s/blog_69c189bf0100mkeu.html这两篇博客)
25.可用第24条知识点解释在类的复制构造函数和赋值构造函数中,为什么该类的另一个对象可以使用私有成员。
class Message
{
public:
Message(const std::string str = ""):contents(str){}
Message(const Message&);
Message& operator=(const Message&);
private:
std::string contents;
};
Message::Message(const Message &m)
{
contents = m.contents;
}
Message& Message::operator=(const Message &m)
{
contents = m.contents;
return *this;
}
从上述代码段中,我们看到contents是类Message下的一个私有成员,在一般函数体内我们是不能直接使用m.contents取值。那么在上述两个构造函数体内为什么能够使用呢?
这要从类的私有成员说起,我们知道私有成员(包括数据和成员函数)只能被该类的成员函数和友元函数访问。那么同一类的不同对象的成员函数能否相互访问之间的私有成员呢?答案是肯定的。从第24个知识点我们知道该类的所有对象共享函数代码——也就是说,在该类的成员函数内部,类的不同对象的私有成员都是可见的,这也就是为什么在上述构造函数中m.contents合法的原因。其实不止构造函数,普通的成员函数也是一样的。比如,我们在类Message定义一个copy(Message &m)函数
void Message::copy(const Message &m)
{
contents = m.contents;
}
这个也是合法的。
其实,在C++ Primer中也提到过每个成员函数具有一个附加的隐含指针this,它的目的就是为了告诉类的成员函数操作的对象是哪个(即当前对象),这样从侧面证实了并不是每个对象都含有一套成员函数的副本。根据这一知识点,我们可以将上述代码段改写为:
void Message::copy(const Message &m)
{
this->contents = m.contents;
}
也就是说“=”号左边的contents其实也是取自当前对象的私有成员,只不过我们一般把this省略。
所以做个总结:私有成员(包括数据和成员函数)只能被该类的成员函数和友元函数访问。不过类的成员函数访问的私有成员可以取自该类的不同对象。(比如上述copy函数中的contents可以取自当前对象也可以取自对象m)
25.好的类设计者会定义直观易用的类接口,而使用者只关心类中影响他们使用的那部分实现。在简单的应用程序中,类的使用者和设计者往往是同一个人。即使在这种情况下,保持角色区分也是有益的。设计类的接口时,应该考虑的是如何方便类的使用;使用类时,设计者就不应该考虑类如何工作——扮演不同的角色,要明确自己身处这个角色的目的。
26.形参表和函数体处于类作用域中,而函数返回类型不一定在类作用域中。(如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外)——类定义体到右括号结束
example
//类定义
class A
{
public:
private:
int ival;
double dval;
};
//类声明
class A;
27.构造函数分两个阶段执行:(1)初始化阶段;(2)普通计算阶段。计算阶段由构造函数体中的所有语句组成,而初始化阶段一般指利用<构造函数初始化列表>进行。
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段之前。如果没有为类成员提供初始化式,则编译器会隐式地使用成员类型的默认构造函数。如果那个类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
28.可以初始化const对象或引用类型的对象,但不能对它赋值。
29.构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序。成员被初始化的次序就是定义成员的次序。
30.定义构造函数,使用默认实参,减少代码重复。
31.如果类包含内置或者自己定义的复合类型成员,则该类不应该依赖于合成的默认构造函数。它应该定义自己的构造函数来初始化这些成员。
32.抑制由构造函数定义的隐式转换:explicit
explicit只能用在类内部的构造函数声明上。在类定义体外部所做的定义上不再重复它,否则会出错。
33.除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为explicit
34.友元不是授予友元关系那个类的成员,所以它们不受其声明出现部分的访问控制影响。不过,通常,将友元声明成组地放在类定义的开始或结尾较好。一般来讲,必须先定义包含成员函数的类,才能将成员函数设为友元。
35.类可以共享static数据成员,也能共享static成员函数。static成员函数没有this形参,它可以直接访问所属类的static成员,但不能直接使用非static成员。static成员函数也不能被声明为虚函数。static数据成员必须在类定义体外部定义。但const static数据成员可以在类定义体内定义,但该数据成员还必须在类的定义体之外进行定义。
example
class Account
{
public:
static double rate();
static void rate(double);
private:
static const int period = 30;
};
const int Account::period //在类定义体外,不需要static关键字