类类型
即使两个类的成员列表完全一致,它们也是不同的类型。对于一个类来说,它的成员和其他任何类(或者任何其他作用域)的成员都不是一回事儿。
Sales_data item1; // 默认初始化Sales_data类型的对象
class Sales_data item1; // 等价 c语言基础而来
类的声明
class Screen; // Screen 类的声明
前置声明,或前向声明。此时可知Screen是一个类类型,但不清楚它到底包含哪些成员。
友元再探
1、 将类定义为友元,Window_mgr的成员可以访问Screen类的私有部分。
class Screen
{
friend class Window_mgr;
}
**友元关系不存在传递性。**若Window_mgr有自己的友元,则这些友元并不能理所当然地具有访问Screen的特权。每个类负责控制自己的友元类或友元函数。
2、将类的成员函数定义为友元。
class Screen
{
friend void Window_mgr::clear(ScreenIndex);
}
3、尽管重载函数名字相同,但在友元时仍需分别声明
4、类和非成员函数的声明不是必须在它们的友元声明之前。
// void f();
struct X
{
friend void f() { std::cout << "aaa" << std::endl; }; // 将友元定义在类中
X() { f(); } // error f 没声明
void g();
void h();
};
void X::g() { return f(); } // error f 没声明
void f(); // 声明了定义在X中的函数
void X::h() { return f(); } // 现在f的声明在作用域中了
**友元声明的作用是影响访问权限,它本身并非普通意义上的声明。**就算是定义了,也必须提供相应的声明从而使函数可见。
名字查找与类的作用域
名字查找:1、首先在名字所在块中寻找其声明语句,只考虑在名字的使用之前出现的声明。2、没找到继续查找外层作用域。3、若最终未找到匹配的声明,程序报错。
类的定义:1、首先编译成员的声明。2、直到类全部可见后才编译函数体。
编译器处理完类中的全部声明后才会处理成员函数的定义。
类型名要特殊处理
当类中的成员使用了外部作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
访问被隐藏的成员
void Screen::dummy_fcn(pos height)
{
cursor = width * height; // 覆盖成员变量,值为形参的
cursor = width * Screen::height; // 通过类名访问成员
cursor = width * this->height; // 通过指针访问成员
}
构造函数再探
Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht*wd, c) {}
Screen(pos ht, pos wd, char c)
{
height = ht;
width = wd;
contents = std::string(ht*wd, c);
}
二者的区别:第一种构造函数直接对数据成员进行了定义并初始化,第二种则是首先执行了一次默认初始化,然后赋了一次新值。
必须使用构造函数初始值的情况
如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,则必须通过构造函数初始值列表为这些成员提供初值。使用构造函数初始值是好习惯!
// 错误
//ConstRef::ConstRef(int ii)
//{
// i = ii;
// // ci = ii; // 不能给const赋值
// // ri = i; // ri 为初始化
//}
// 正确,显式地初始化引用和const成员
ConstRef::ConstRef(int ii): i(ii), ci(ii), ri(i){}
成员初始化的顺序
成员的初始化顺序与它们在类定义中的出现顺序一致,构造函数初始值列表中初始值的前后位置关系,不会影响实际的初始化顺序。
class X
{
public:
int i;
int j;
X(int val);
};
// 此例中,i先被初始化,此时j并未定义。
//X::X(int val) : j(val), i(j){} // error
X::X(int val) : i(val), j(val){} // error
构造函数初始值的顺序应与成员声明顺序一致。尽量避免使用某些成员初始化其他成员。
默认实参和构造函数
class X
{
public:
int i;
int j;
X(int val = 0);
};
// ---- main ------
class X a {};
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。
委托构造函数
委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。
若受委托的构造函数体包含代码,会执行代码后,才将控制权交还给委托者函数体。
使用默认构造函数
Sales_data obj(); // 声明了一个函数而非对象
Sales_data obj2; // obj是一个对象,默认构造函数
Sales_data obj2{}; // 同上,有参数可以放在大括号里
隐式的类类型转换
能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
class Test {
public:
Test(string str) { }
Test(string str, string str2) { }
};
void printTest(Test test) { std::cout << "aaa" << std::endl; }
int main()
{
printTest(string("aa"));
return 0;
}
当构造函数包含两个参数时,可以使用大括号初始化(列表初始化),直接匹配两个参数的构造函数,不属于隐式类型转换。
printTest({ string("aa"), string("aa") });
只允许一步类类型转换
编译器只会自动地执行一步类型转换。
printTest("aa");
此种情况需要使用两步转换,先将"aa"转为string,然后再将(临时)string转换成Test。
// 显式地转换成string,隐式地转换成Test
printTest(string("aa"));
// 隐式地转换成string,显式地转换成Test
printTest(Test("aa"));
// 全部显式地转换
printTest(Test(string("aa")));
抑制构造函数定义的隐式转换
可通过将构造函数声明为 explicit 阻止隐式转换。explicit 只对一个实参的构造函数有效。
explicit Test(string str) { }
// 隐式转换失败
printTest(string("aa"));
只能在类内声明构造函数时使用,在类外部定义时不应重复
直接初始化&拷贝初始化
聚合类
1、所有成员都是public的。2、没有定义任何构造函数。3、没有类内初始值。4、没有基类,也没有virtual函数。如下:
struct Data
{
int ival;
string s;
};
聚合类的初始化顺序必须与声明的顺序一致。
Data val1 = { 0, "anna" };
// Data val2 = { "anna", 0 }; // 错误,无法使用 "anna" 初始化 ival, 无法使用 0 初始化 s
Data val3 = { 0 }; // 缺省的内容被值初始化,s 为空字符串
// Data val4 = { 0, "anna", 1 }; // 初始值列表元素个数,不能超过类成员数量
缺点:1、要求类所有成员都是public。2、将初始化交给了类的用户。3、添加和删除成员后,初始化语句都需要更新。
字面值常量类
constexpr函数的参数和返回值必须是字面值类型。除算术类型、引用和指针外,某些类也是字面值类型。
数据成员都是字面值类型的聚合是字面值常量类。若不是聚合类,但其符合下属要求时,也是一个字面值常量类:1、数据成员都必须是字面值类型。2、类必须至少含有一个constexpr构造函数。3、若一个数据成员含有类内初始值,则内置类型成员的初始值必须是一条常量表达式;若成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。4、类必须使用析构函数的默认定义。
constexpr构造函数
构造函数不能是const的,但字面值常量类的构造函数可以是constexpr函数。字面值常量类至少提供一个constexpr构造函数。
class Debug{
public:
constexpr Debug() = default;
}
静态成员
静态成员可以是public或private的。静态数据成员的类型可以是常量、引用、指针、类类型等。
静态成员存在于任何对象之外,类似于全局变量。对象中不包含任何与静态数据成员有关的数据。静态成员函数也一样。
通过作用域运算符直接访问静态成员。
类名::方法名()
通过对象访问静态成员
对象(引用).方法名()
指针(指向对象)->方法名()
成员函数可以直接访问静态成员。
定义静态成员
static 关键字出现在类内部的声明语句。外部定义静态成员时,不能重复使用static。
静态成员的类内初始化
static const 类型的数据成员只有在它们是整型或枚举类型时才能在类声明中初始化。类的静态成员不应在类的内部初始化。
class Example {
public:
double rate = 6; // ok
const double rate2 = 6; // ok
// static const double rate3 = 6; // 错误, 类内初始值设定项的静态数据成员必须具有不可变的常量整型
static constexpr double rate4 = 6; // 使用constexpr可以,编译时常量
static const int rate5 = 6; // const 整型可以在类内部初始化
static constexpr int rate6 = 6; // constexpr 整型可以在类内部初始化
static int rate7; //// 声明静态变量
};
尽管一个常量静态数据成员在类内部被初始化了,通常情况下也应该在类的外部定义一下该成员
#include "example.h"
constexpr double Example::rate4 ; // 再.cpp文件中提供定义
int Example::rate7 = 555; // 静态变量在类外初始化
static const 必须在类内提供初始化,不能在类的实现文件中再进行初始化。
class Example {
public:
static const int vecSize = 4; // static const
static std::vector<double> vec; // 只能在类外初始化
};
#include "example.h"
vector<double> Example::vec(Example::vecSize);
静态成员于普通成员的区别
1、静态数据成员可以是不完全类型,它的类型可以就是它所属的类类型。
class Test {
public:
Test(string str) {};
private:
static Test a; // 静态成员可以是不完全类型
Test* b; // 指针成员可以是不完全类型
// Test c; // 错误, 数据成员必须是完全类型
};
2、静态成员可以作为默认实参。
class Test {
public:
Test(char = b) {};
private:
static const char b;
};
文章评论