C++ Primer 第十二章 类
简单地说,类就是定义了一个新的类型和一个新作用域。
12.1 类的定义和声明
类由类成员组成。类成员包括属性,字段,成员函数,构造函数,析构函数等组成。
类设计应该遵从抽象封装性。
类抽象性指对于类的使用者来说只需知道类接口即可使用类功能。类的具体实现由设计者负责。即使某个功能发生了变更但由于使用者是以接口方式调用类所以用户代码无需做任何修改。
类封装性指类用户只需知道类的功能无需了解具体实现。实现代码对用户来说不可见。
C++类没有访问级别限限制,定义类时不能用public 或 private 做修饰。类成员有访问级别,可以定义 public protect private
public:
string name;
// 给类定义别名类型成员 index 由于别名要在外部访问所以一定要定义在 public
typedef std::string::size_type index;
// 内部定义的函数,等价于inline
char get() const { return contents[cursor]; }
// 内部声明一个成员函数(无定义),且函数是内联的inline表示在编译时该声明会被替换成定义语句
// 内部声明一个成员函数(无定义)
index get_cursor() const;
};
// 定义类 Screen 的成员函数 get 具体实现
char Screen::get(index r, index c) const
{
index row = r * width; // compute the row location
return contents[row + c]; // offset by c to fetch specified character
}
// 定义类 Screen 的成员函数 get_cursor 具体实现,且是内联的
{
return cursor;
}
注意:类的inline修饰符可以放在类内部申明也可以放在外部定义。一般放在内部声明便于理解。
类定义完毕后一定要加上封号结束符 ;。
类数据成员只允许声明不允许定义;
可以声明类而不定义它。成为前向声明又叫不完全类,这样的类无法定义实例也无法使用成员。一般用来处理类相互依赖的情况。定义了类就能定义类对象:myclass obj; 一定要注意不能是 myclass obj() ; 类对象定义时会分配内存空间,每个类都有自己的空间相互间不受影响。
12.2 隐含的this指针
类对象包含一个 this 指针指向自身(当前的实例对象)且无法更改指针指向。在普通的非 const 成员函数中,this 的类型是一个指向类类型的 const 指针。可以改变 this 所指向的值,但不能改变 this 所保存的地址。在 const 成员函数中,this 的类型是一个指向 const 类类型对象的 const 指针。既不能改变 this 所指向的对象,也不能改变 this 所保存的地址。
基于成员函数是否为 const,可以重载一个成员函数;同样地,基于一个指针形参是否指向 const可以重载一个函数。
{
public:
mycls(){}; // 想要定义 const mycls a; 必须要显示定义默认构造函数
mycls &Get(){ return *this; };
const mycls &Get() const { return *this; }; // 想如果const函数返回this引用或指针; 必须要返回const指针或引用,因为无法用const对象(this)初始化非const对象。
};
const mycls b;
mycls b1 = c.Get(); // 调用const版Get函数
const mycls b2 = c.Get(); // 调用const版Get函数
// b1 b2 定义时会调用类的拷贝函数。b1,b2是Get返回值的副本,b1还会将常量副本转变成变量
mycls &b3 = c.Get(); // 错误,不能用 const &mycls 初始化 &mycls (指针或者引用类型不能用常量初始化变量)
const mycls &b4 = c.Get();
mycls a;
mycls a1 = a.Get(); // 调用非const版Get函数
const mycls a2 = a.Get(); // 调用非const版Get函数
由此可见调用那个版本和调用对象是否const有关系,const对象会调用const版本,非const对象会调用非const版本。
引用网上的总结:
成员函数具有const重载时,类的const对象将调用类的const版本成员函数,类的非const对象将调用非const版本成员函数。
如果只有const成员函数,类的非const对象也可以调用const成员函数。 ——这个思路来描述很囧。下同。
如果只有非const成员函数,类的const对象…额,不能调用非const成员函数。 ——其实跟上一句的意思是一样的:const对象只能调用它的const成员函数。
总的来说,就是当我们调用一个成员函数时,编译器会先检查函数是否有const重载,如果有,将根据对象的const属性来决定应该调用哪一个函数。如果没有const重载,只此一家,那当然就调用这一个了。这时编译器亦要检查函数是不是没有const属性而调用函数的对象又有const属性,若如此,亦无法通过编译。
还有一点非常重要,想要定义类的const对象必须显示定义对应构造函数,无法依赖系统自动分配的构造函数。
12.3 类作用域
每个类对象独有独立的作用域。
C++类定义一般分两部分,类成员申明(类定义内部)和类成员定义(类定义外部)。虽然成员定义在类外部但还是可以像类内部定义一样使用类所有成员。
12.4 构造函数
构造函数是特殊的成员函数。在类对象定义时被调用。 不能通过定义的类对象调用构造函数,构造函数可以定义多个或者说构造函数允许重载。
如果没有定义任何构造函数,系统就会给类分配一个无参的默认构造函数,类只要定义了一个构造函数,编译器也不会再生成默认构造函数。只有当一个类没有定义构造函数时,编译器才会自动生成一个默认构造函数
定义类对象时不能写成 Sales_item myobj(); 编译器会理解成:一个返回 Sales_item 类型叫 myobj的函数声明。 正确写法是去掉后面的括号。
构造函数后面不允许定义成 const,这样定义会产生语法错误: Sales_item() const {};
构造函数在执行时会做类数据成员的初始化工作。从概念上讲,可以认为构造函数分两个阶段执行:(1)初始化阶段;(2)普通的计算阶段。计算阶段由构造函数函数体中的所有语句组成。
不管成员是否在构造函数初始化列表中显式初始化,类类型的数据成员总是在初始化阶段初始化。初始化发生在计算阶段开始之前。
{
public:
mycls()
{
age = 12; name = "tom";
};
mycls(int i):age(i)
{
age = 12 + i; name = "tom";
};
private:
int age;
string name;
};
mycls obj1 ;使用无参构造函数,虽然构造函数并没有显示初始化数据成员但类类型name还是会被初始化成默认值name初始化为"" age未初始化(其值是个随机数),初始化后构造函数重新赋值,最终age=12, ame = "tom" ;
mycls obj2(4) ; 用构造函数参数初始化 age = 4, name = "",构造函数重新赋值,最终age=16, name = "tom" ;
如果数据成员是自定义类类型,如果不显示初始化则类一定要有默认构造函数否则编译错误,成员被初始化的次序就是定义成员的次序。第一个成员首先被初始化,然后是第二个,依次类推。
默认情况下可以用单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式转换。
{
public:
int i;
mycls(int i){ };
explicit mycls(string s){ };
};
mycls obj(2) ; 也可以这样使用这个构造函数 mycls obj = 2; 这里做了一个类型转换,但是这样的写法很不直观。
可以通过将构造函数声明为 explicit,来防止在需要隐式转换的上下文中使用构造函数:mycls obj("tom"), 无法用 mycls obj = "tom" 因为转换被禁止,通常,除非有明显的理由想要定义隐式转换,否则,单形参构造函数应该为 explicit。
explicit 关键字只能用于类内部的构造函数声明上。在类的定义体外部所做的定义上不再重复它。
12.5 友元
友元机制允许一个类将其非公有成员的访问权限授予指定的函数或类。
将类作为自己的友元类如下定义
class me
{
friend class he;
private:
int i;
string s;
};
class he
{
public:
void show(me &it)
{
cout << it.i << it.s << endl;
};
};
类he是me的友元类,所以he中可以访问me的私有成员i和s;
将类成员作为另一个类的友元函数情况比较复杂,需要用到前面讲过的前向声明(两个类之间有互相依赖关系)
class me; // 先要前向声明类
class he // 友元类需要目标类做参数由于目标类已声明所以可以使用类引用或者指针--show(me &it)方法中的参数
{
public:
void show(me &it);
};
class me // 目标类需要声明类的的成员show作为自己的友元函数,he在上面做了成员声明所以成员show(me &it)可用
{
friend void he::show(me &it);
private:
int i;
string s;
};
void he::show(me &it) // 友元方法中使用目标类私有成员,目标类上一步定义了私有成员因此这里成员可用
{
cout << it.i << it.s << endl;
};
声明定义的顺序非常重要,一定要理解否则会产生各种未定义类编译错误。
12.6 static 成员
类的静态成员不属于任何一个类对象所以静态成员中(主要是静态方法)不包含this指针因此也无法声明成const函数,它是所有类对象共享数据。
不同于其他语言的访问方式,静态成员既可以通过类型访问:myclass::staticname() 也可以通过类对象(对象,指针或者引用)访问:obj.staticname()。
一般来说类数据成员在类定义体内不能初始化化,但有个特例 const static 数据成员就可以在类的定义体中进行初始化 。
类非 static 数据成员在类体内声明,必须要在类体外定义。
{
public:
void show()
{
cout << i << j << endl;
};
private:
static int i;
const static int j = 1;
};
int me::i = 1; //这一步不能少,否则编译器检查到show()方法中使用i类体外又没有定义会产生编译错误
需要强调const static 体内初始化但体外定义也不能少,但是如果体外不作定义在定义类对象时会产生编译异常。
最后补充一点关于类成员函数的重载。函数重载不但可以用参数类型和参数个数不同来重载,还可以通过const修饰变量来实现函数重载,即函数名称、参数个数、参数类别都一样,唯一的区别在于变量是否为const修饰。用 const做重载依据有两种类型:const参数,const函数:
{
public:
A() {}
void func(int *a) //相当于void func(int *a, A *this)
{
std::cout << "_func_int_ptr_" << std::endl;
}
void func(const int *a) //相当于void func(const int *a, A *this)
{
std::cout << "_func_const_int_ptr_" << std::endl;
}
void func(int *a) const //相当于void func(int *a, const A *this)
{
std::cout << "_const_func_int_ptr_" << std::endl;
}
void func(const int *a) const //相当于void func(const int *a, const A *this)
{
std::cout << "_const_func_const_int_ptr_" << std::endl;
}
};
int main(int argc, char* argv[])
{
A a;
int nValue = 3;
const int nValueCnst = 3;
a.func(&nValue);
a.func(&nValueCnst);
const A aa;
aa.func(&nValue);
aa.func(&nValueCnst);
return 0;
}
其输出为:
_func_int_ptr_
_func_const_int_ptr_
_const_func_int_ptr_
_const_func_const_int_ptr_
从这里可以看出,通过const修饰一个变量可以实现同名称函数的重载。另外,一个类的非const对象可以调用其const函数(如果只定义了const函数版本,非const对象就可以调用const成员函数)。但const 对象无法调用非 const 函数(非const函数可能会修改 this 而 this 是 const对象,有潜在BUG)。
总结起来,可以初始化的情况有如下几个地方:
1. 类型为const 且 static 的整型变量可以在定义时直接初始化值(只能用赋值初始化不能用直接初始化) 也可以在体外。
2. 普通const常量(不包含第一种情况)必须要在构造函数初始化列表中初始化值。
3. 只要有static修饰,必须要在类定义体外定义并给值(第一种情况时也需要这么做,不过只能定义不能再给值) static数据不属于任何对象所以不能出现在构造函数初始化列表。
4. 普通的变量可以在构造函数的内部,通过赋值方式进行。当然这样效率不高。
5. 数组成员不能在初始化列表里初始化的。只能自动调用数组的无参构造函数(可以在构造函数内操作数组)。
{
public:
int a;
const int b;
static int c;
static const d = 1; // 体内定义时不能用直接初始化给值
static const e;
obj():a(0),b(0){};
};
int obj::c = 2 ; // 体外定义(不能出现statci关键字)
const int obj::d ; // 体外定义,d已经在体内给只所以只需定义不能给值(const 必须)
const int obj::e = 1 ; // 定义并给值