每个类都定义了自己的新作用域和唯一的类型,两个不同的类具有两个的类作用域
即使两个类具有完全相同的成员列表,它们也是不同的类型,每个类的成员不同于任何其他类(或任何其他作用域)的成员
在类作用域之外,成员只能通过对象或指针分别使用成员访问操作符 . 或 -> 来访问
.操作符左边的操作数是一个类对象
->操作符左边的操作数是指向类对象的指针
形参表和函数体处于类作用域中
在定义于类外部的成员函数中,形参表和成员函数体都出现在成员名之后,这些都是在类作用域中定义,所以可以不用限定而引用其他成员
函数返回类型不一定在类作用域中
如果函数在类定义体之外定义,则用于返回类型的名字在类作用域之外
一般名字查找
1. 在使用该名字的块中查找名字的声明,只考虑在该项使用之前声明的名字
2. 如果找不到该名字,则在包围的作用域中查找
3. 如果找不到任何声明,则程序出错,在 C++ 程序中,所有名字必须在使用之前声明
类定义实际上是在两个阶段中处理:
1. 首先,编译成员声明
2. 只有在所有成员出现之后,才编译它们的定义本身
类成员声明的名字查找
1. 检查出现在名字使用之前的类成员的声明
2. 如果第 1 步查找不成功,则检查包含类定义的作用域中出现的声明以及出现在类定义之前的声明
必须在类中先定义类型名字,才能将它们用作数据成员的类型,或者成员函数的返回类型或形参类型
一旦一个名字被用作类型名,该名字就不能被重复定义
类成员定义中的名字查找
1. 首先检查成员函数局部作用域中的声明
2. 如果在成员函数中找不到该名字的声明,则检查对所有类成员的声明
3. 如果在类中找不到该名字的声明,则检查在此成员函数定义之前的作用域中出现的声明
如果类的成员被屏蔽了,可以通过用类名来限定成员名或显式使用 this 指针来使用它
构造函数
构造函数不能是const 的,不管对象是否为 const,都用一个构造函数来初始化化该对象
构造函数初始化列表以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式
从概念上讲,可以认为构造函数分两个阶段执行:
1. 初始化阶段
2. 普通的计算阶段 计算阶段由构造函数函数体中的所有语句组成
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化,初始化发生在计算阶段开始之前
在构造函数初始化列表中没有显式提及的每个成员,使用与初始化变量相同的规则来进行初始化:运行该类型的默认构造函数,来初始化类类型的数据成员;内置或复合类型的成员的初始值依赖于对象的作用域:在局部作用域中这些成员不被初始化,而在全局作用域中它们被初始化为 0
有些成员必须在构造函数初始化列表中进行初始化,对于这样的成员,在构造函数函数体中对它们赋值不起作用
对非类类型的数据成员进行赋值或使用初始化式在结果和性能上都是等价的
以初始化 const 对象或引用类型的对象,但不能对它们赋值,在开始执行构造函数的函数体之前,要完成初始化
初始化 const 或引用类型数据成员的唯一机会是构造函数初始化列表中
必须对任何 const 或引用类型成员以及没有默认构造函数的类类型的任何成员使用初始化式
构造函数初始化列表仅指定用于初始化成员的值,并不指定这些初始化执行的次序,成员被初始化的次序就是定义成员的次序
初始化的次序常常无关紧要,然而,如果一个成员是根据其他成员而初始化,则成员初始化的次序是至关重要的
按照与成员声明一致的次序编写构造函数初始化列表是个好主意,此外,尽可能避免使用成员来初始化其他成员
一般情况下,通过(重复)使用构造函数的形参而不是使用对象的数据成员,可以避免由初始化式的执行次序而引起的任何问题
初始化式可以是任意表达式
只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数
如果类包含内置或复合类型的成员,则该类不应该依赖于合成的默认构造函数,它应该定义自己的构造函数来初始化这些成员
类通常应定义一个默认构造函数
实际上,如果定义了其他构造函数,则提供一个默认构造函数几乎总是对的,通常,在默认构造函数中给成员提供的初始值应该指出该对象是“空”的
可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换
可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数
explicit 关键字只能用于类内部的构造函数声明上,在类的定义体外部所做的定义上不再重复它
当构造函数被声明 explicit 时,编译器将不使用它作为转换操作符
通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit,将构造函数设置为 explicit 可以避免错误,并且当转换有用时,用户可以显式地构造对象
#include <iostream>
#include <string>
using namespace std;
class First {
public:
int memi;
double memd;
};
class Second {
public:
int memi;
double memd;
};
typedef double Money;
class Account {
public:
Account(){}
//explicit Account(Money money):bal(money){}
Account(Money money):bal(money){}
Money balance() { return bal; } // uses global definition of Money
bool equals(Account a) const {
return bal - a.balance();
}
private:
//typedef long double Money; error: cannot change meaning of Money
Money bal;
// ...
};
// Note: This code is for illustration purposes only and reflects bad practice
// It is a bad idea to use the same name for a parameter and a member
int height;
class Screen {
public:
void dummy_fcn_1(int height) {
cursor = width * height; // which height? The parameter
}
// bad practice: Don't hide names that are needed from surrounding scopes
void dummy_fcn_2(int height) {
cursor = width * ::height;// which height? The global one
}
//Screen() const{} error! constructors may not be `const'
private:
int cursor;
int height, width;
};
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): ci(ii),ri(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
}
class X {
int i;
int j;
public:
// run-time error: i is initialized before j
X(int val): j(val), i(j) {
cout << "i = " << i << endl;
}
};
int main()
{
First obj1;
//Second obj2 = obj1;e rror: obj1 and obj2 have different types
X x(2); // i = ?
Account a = Account(); //ok: create an unnamed, empty Account use to initialize a
Account a1(98.1);
bool b = a1.equals(99.9); //Implicit Class-Type Conversions if explicit this will be an error
cout << boolalpha << b << endl;
return 0;
}