c++ primer plus笔记(8)类基础
一、类(class)的声明:
class World //类名一般习惯大写
{
float m_mass; //类成员的默认访问类型为private
char m_name[20]; //类成员变量其命名习惯在之前加上m_
void set_mass(){m_mass=m_name[1];} //在类内直接定义的方法会被默认为内联方法
public:
void tellall(); //使用public声明的成员函数,公有接口
};
①类(class)声明时使用public、private和protected关键字控制外部对其成员的访问权限,从而实现对数据的封装:
1>public(公有)成员对外界表现为可见;
2>private(私有)成员对外界表现为不可见;
3>protected(保护)成员对外界表现为不可见,其只在继承时体现出其特性,故不做详细讨论;
②因此类设计的基本原则是:公有用户接口(public)与私有数据、方法(private)应当分开。用户对类的所有操作都应当只能使用类对象暴露在外的公有接口完成。
//类的声明一般在头文件中完成。
二、类的成员函数:
成员函数的一大共同特点就是他们都含有this指针,因此调用时需要指明其所属对象。
//this指针是指向所属对象本身的指针:
const World& World::w_add()
{
m_mass++;
...;
return *this; //返回该对象本身
}
this是对象本身的地址,而*this是地址处的对象本身。①类方法:
1>类方法的定义:
void World::tellall() //使用域运算符(::)标识方法所属的类
{
...;
}
//类方法的定义(类的实现)一般在实现文件中完成。
2>类方法的调用:
类方法使用成员运算符(.)在该类的某一对象中被调用:
World w;
w.tellall(); //调用tellall方法
3>const后置的类方法:
声明类方法时使用后置const表明这个方法不会对对象(但可对对象外的数据)做出任何修改:
class World
{
...;
public:
void funct()const; //该方法只能使用但没有权限修改对象的任何部分
...;
};
②构造函数(constructor):
1>构造函数(constructor)用于创建和初始化类对象的成员,在公有接口(public)部分声明:
class World
{
...;
public:
World(...); //构造方法名与类名相同
...;
};
//构造函数没有返回值和声明类型(因为不需要);
//构造函数同样可以被多次重载,以适应对象的初始化要求;
//与类方法一样,构造方法的定义也在实现文件中完成。定义构造方法时,其形参名不可与成员名一致:
class World
{
char name[20];
public:
World(...);
...;
};
int main()
{
...;
return 0;
}
World::World(const char* name)
{
strcpy_s(name,20,name); //编译器:???
}
这也就说明了为什么要在成员名前加上m_:
World::World(const char* name)
{
strcpy_s(m_name,20,name); //编译器:懂你意思
}
//虽然与类方法诸多相似,但构造函数并不是类方法,一个重要的区别是:构造函数在对象被创建之前调用(这很容易理解,因为调用它的目的正是为了构造对象),换句话说以下的写法是错误的:
w.World(); //构造函数不是类方法,不能通过成员运算符调用
2>默认构造函数是未提供初始值时,用来创建对象的构造函数:
class World
{
...;
public:
World(); //默认构造函数
...;
};
World w; //调用默认构造函数
当没有显式声明任何构造函数时,编译时将自动提供一个(不进行任何操作的)默认构造函数:
class World
{
float m_mass;
char m_name[20];
public:
void tellall();
}; //不显式声明任何构造函数
提供格式可能与以下形式接近:
World::World(){} //只创建对象而不进行任何初始化操作
这种情况下,意味着声明一个该类对象可能与以下操作接近:
World w;
int a; //只声明创建变量而不初始化
使用一块未被初始化的内存,这种行为的危险性是显而易见的(尤其是在使用指针和heap区内存的类中)。
//值得注意的是,任何形式的构造函数被显示声明在类中,都不会导致该行为:
class World
{
float m_mass;
char m_name[20];
public:
World(char*); //提供任何形式的构造函数(不一定是默认构造函数)
void tellall;
}
这将导致以下声明成为非法:
World w; //此时必须使用World w(char*)创建对象
//另一个值得注意的问题是,(隐式的)调用默认构造函数的语句不得使用圆括号:
World w(); //声明一个返回值为World类型的函数,而非调用默认构造函数
3>转换构造函数:只有一个参数的构造函数又作为转换函数而存在:
class World
{
float m_mass;
char m_name[20];
public:
World(); //默认构造函数
World(const char*); //只有一个const char*参数的构造函数又被用作const char*到World的自动类型转换
void tellall;
}
转换函数在需要将别的类型自动转换为类类型的场合被(隐式)调用:
World w; //调用默认构造函数
w = "abc"; //或World w = "abc";
//当w="abc"被执行时,其执行过程为:先调用World(const char*)创建一个临时的、用"abc"初始化的World对象,随后采用浅度复制的方法将临时对象逐成员赋值给对象w,最后销毁该临时对象;
//正是因为转换构造函数牵涉到临时变量及执行浅度复制的特性,所以所有使用heap区内存的类(使用new的类)都应当谨慎处理这一类只有一个参数的构造函数;
//为避免不必要的麻烦,可以使用explicit关键字声明构造函数以关闭这种隐式转换:
explicit World(const char*); //关闭隐式转换特性
此时默认情况下该构造函数不会用作类型转换,只有显式转换(使用强制类型转换语法)时才会调用:
World w; //调用默认构造函数
w = (World)"abc"; //或World w = (world)"abc";
③析构函数(destructor):
1>析构函数(destructor)用于清理到达生命周期的对象,释放其本身及其内包含的指针指向的内存:
class World
{
...;
public:
World(...); //构造方法名与类名相同
~World(); //析构函数名为~类名
...;
};
//与构造函数不同,析构函数不仅没有返回值和声明类型,它连参数都没有;
//与构造函数不同,析构函数函数只有一个,不存在重载析构函数这一说法;
2>析构函数的定义:
World::~World()
{
...; //...可能为空白,也可能包含delete操作(类使用heap区内存的情况)
}
3>当没有显式声明任何析构函数时,编译时将自动提供一个(不进行任何操作的)隐式析构函数:
class World
{
...;
public:
World(...);
...; //不显式声明析构函数
};
提供格式就像这样:
World::~World(){}
如果类使用了指向heap区的指针,那么这种行为就存在潜在的风险(堆区溢出)。
4>析构函数一般不应在代码中显式调用。例外是释放使用定位new存放的对象:
typedef char* byte;
byte buffer = new char[512]; //使用定位new申请一块521字节的内存,命名该区域为buffer
World* p_w = new(buffer)World; //使用定位new在申请所得的buffer区存放一个World对象,并用指针p_W指向它
...;
p->~World(); //手动调用World对象的西固函数,将其所在的内存释放
//由于stack的结构特点(FILO),析构函数的调用顺序与构造函数刚好相反。换句话说,最后被创建的对象,将最先被释放。
④转换函数(conversion function):
转换函数(conversion function)是用户定义的强制类型转换:
class World
{
float m_mass;
char m_name[20];
public:
World(); //默认构造函数
World(const char*); //只有一个const char*参数的构造函数又被用作const char*到World的自动类型转换
operator const char*(); //用户定义的World到const char*的自动类型转换
void tellall;
}
转换函数在需要将类类型自动转换别的类型为的场合被(隐式)调用:
World w;
char array[20] = w; //operator const char*()被调用
//与析构函数类似,转换函数不写返回值类型和参数。但不同点是,析构函数没有返回值,而转换函数由于在operator后已经指定返回类型故不再指定:
World::operator const char*() //转换函数的定义方式也与别的类方法类似
{
...;
return ...;
}
//构造转换函数与转换函数的关系正好相反:构造转换函数:TYPE→classtype
转换函数:classtype→type
//转换函数也是(且必须是)类方法,这意味着它必须通过指定对象来调用。
//与转换构造函数类似,也可以通过explicit关键字声明转换函数以关闭这种隐式转换:
explicit operator const char*(); //关闭隐式转换特性
此时默认情况下该类型不会发生这样的隐式类型转换,只有显式转换(使用强制类型转换语法)时才会调用:
World w;
char array[20] = (const char*)w;
//不幸的是,只有C++11支持使用explicit关闭转换函数的隐式转换特性。(也就是说对于不支持C++11标准的编译器来说,使用转换函数带来的风险将会增大)
⑤静态成员函数(类级方法):
静态成员函数是使用static声明的成员函数,其又被称为类级方法:
class World
{
float m_mass;
char m_name[20];
public:
World(char*);
void tellall;
static int func(); //静态成员函数
}
静态成员函数并不是类方法,因为其不含this指针,这直接导致了其以下特性:
1>静态成员函数不能通过任何对象调用(因为它不属于任何对象而是属于整个类)。但如果是在public部分声明则可使用域运算符(::)指明其所属的类调用(这也是一般的使用方法,作为操作类级标记的手段,改变所有属于该类的对象的行为);
2>静态成员函数只能访问和操作静态成员(类级成员);
3>静态成员函数的行为对从属于该类的所有实体对象起作用;
三、类的数据:
①类的普通成员:遵循访问类型,指明对象使用成员运算符(.)对其进行访问和使用。(该咋地咋地)
②类的静态成员:静态成员是使用static声明的成员数据,其又被称为类级成员。
1>静态成员变量:
class World
{
float m_mass;
char m_name[20];
public:
static int time; //静态成员变量
World(char*);
void tellall;
}
与静态成员函数类似,静态成员变量如果是在public部分声明则也可使用域运算符(::)指明其所属的类使用。
与静态成员函数类似,静态成员变量不从属于任何对象,而是属于整个类,它的改变对从属于该类的所有对象都可见。
//静态成员变量在类声明中被声明,在包含类方法实现的文件中被初始化。
2>静态成员常量:
class World
{
float m_mass;
char m_name[20];
public:
static const int time = 1; //静态成员常量
World(char*);
void tellall;
}
与静态成员函数类似,静态成员常量如果是在public部分声明则也可使用域运算符(::)指明其所属的类使用。
与静态成员函数类似,静态成员常量不从属于任何对象,而是属于整个类,它是从属于该类的所有对象都可以使用的公共资源。
//静态成员常量的特殊之处在于:它可以在类声明中初始化。(这真是太厉害了)
类是用于产生对象的方案描述,写在其中的所有内容都没有用于储存它们的内存空间(换句话说没有对象的类没有实例存在于内存中),故在类声明中提供实际的初始化值本来就是违背“方案描述”这一逻辑的。
但符号常量作为一种资源(或者说标记)有必要可以让整个类都能使用,而由于常量的特殊性(一旦初始化就不可以再次改变),必须在声明的同时予以初始化,故静态成员常量可以在类声明中初始化,其值不与该类的任何对象存放在一起,而是单独存放于静态区。(所以说真是太厉害了)
//C++11之前的标准,能使用static在类中声明并初始化的只有整形,而double不可以。
③作用域为类的枚举:
在类中声明的枚举,其中被枚举的符号常量作用域为整个类:
class World
{
float m_mass;
char m_name[20];
public:
World(); //默认构造函数
World(const char*);
enum size{s,m,l}; //作用域为类的枚举
void tellall;
}
通常使用这种枚举的场合与静态成员常量类似(建立类级标记,成为所有对象的公用资源等),所以通常不必写明枚举变量名,只需创建枚举的符号常量即可。因此常用的写法大概会是这样:
enum{s,m,l}; //只创建符号常量而不创建枚举变量名
或者是这样:enum{Months = 12}; //只创建符号常量而不创建枚举变量名
这种写法得到的结果就和直接创建静态成员常量一样:
enum{Months = 12}; //只创建符号常量而不创建枚举变量名
static const int Month = 12; //可以认为二者功能相同
//被枚举的符号常量作用域为整个类意味着这可能导致一些问题,在多个枚举变量中声明同名符号常量时将因为无法区分而出错:
enum egg{small,medium,large};
enum T_shirt{small,medium,large};
//这种情况下,由于枚举的符号常量名重复,将无法通过编译
C++11为此提出解决方案(作用域内枚举):
enum class egg{small,medium,large};
enum struct T_shirt{small,medium,large};
//在枚举名前加上class或struct关键字,以限制被枚举的符号常量的作用域
被加上class或struct关键字的类枚举,其中被枚举的符号常量作用域不再为整个类,而是仅限于该枚举变量中。
所以这样的符号常量使用时自然也应该加上范围说明:
egg a = egg::large;
T_shirt = T_shirt::large;
//用域运算符(::)标明其所属的域
//由于需要标明范围,故被加上class或struct的枚举变量不可匿名;
//作用域内的枚举与普通枚举的一个重要区别是,其不可隐式类型转换:
int i = egg::large; //不允许
④在类中定义的结构体:
要说明这个问题,首先要清楚结构体(struct)和类(class)的关系:
1>事实上,C++中结构体的实现机制与类十分接近。与类一样,结构体也是可是使用构造函数的,结构体成员也可使用private、public、protected控制其访问类型。
2>要说唯一的区别,结构体(struct)中成员的默认访问类型为public,而类中成员的默认访问类型为private。区别虽小,但很明显默认公开数据违背了OOP封装和数据隐藏的原则,所以结构体通常不用做类描述。(细微的差别导致待遇的巨大不同)
所以在类中声明定义结构体,这个行为和在类中定义另一个类是高度相似的,也即嵌套类:
class Linked_list
{
struct Node
{
Node* next;
item deta;
}
...;
};
class Linked_list
{
class Node
{
Node* next;
item data;
}
};
所以使用类中定义的结构体规则与使用嵌套类相同。