声明、定义
// 类是一个作用域。
// 编译器编译类分两步:
// 1、编译成员(变量、函数)声明
// 2、编译成员函数体。
struct Sales_data { // 也可以用class,没有深层次的区别,仅仅是默认访问权限不同。
public:
// 以下是类成员函数,必须在类内声明,但定义可在类外部
// 在类内部定义,隐式声明为inline
// const 修饰 this,即为const (Sale_data* const),括号内是this的类型。
// 这个const修饰的函数称为常量成员函数
std::string isbn() const {
// 隐式调用return this.bookNo;
// this表示当前调用这个函数的对象,Sale_data* const类型,常量指针。
return bookNo;
}
Sale_data& combine(const Sales_data&);
double avg_price() const; // 平均价格
private:
std::string bookNo; // isbn编号
unsigned units_sold = 0; // 销量,类内初始值,注意不是初始化(在构造函数完成)
double revenue = 0.0; // 总销售收入,类内初始值。
}
// 类外部定义成员函数
double Sales_data::avg_price() const { // const必须和声明时一样。
if(units_sold) return revenue / units_sold;
return 0;
}
// combine是为了实现类似+=复合赋值运算符,应尽量模仿+=,因此返回引用。
Sales_data& Sales_data::combine(const Sales_data& rhs){
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this; // *this返回引用
}
// 非类成员函数,在类外部声明
// 这些函数接口被类成员函数使用,因此声明在同一个头文件中。
Sales_data add(const Sales)data&, const Sales_data&);
std::ostream& print(std::ostream&, const Sales_data&);
std::istream& read(std::istream&, Sales_data);
构造函数
初始化类对象数据成员(成员变量)的成员函数,只要对象被创建,就会执行构造函数进行初始化。
// 构造函数名必须和类名相同,不能有返回值。
// 可以有不同构造函数(重载,区别在形参列表上)。
Sales_data::Sales_data(......) // 不能有const声明
: bookNo("") // 构造函数初始值列表,可为空(走默认构造函数初始化逻辑)
, units_sold(0) // 没有在列表中的,走默认构造函数初始化逻辑。
, revenue(0.0)
{
// 这是很不可取的成员初始化方法,这并不是真正的初始化,仅仅是初始值列表之后的赋值。
bookNo = "";
units_sold = 0;
revenue = 0.0;
}
默认构造函数
当没有显式定义构造函数,编译器就会隐式创建一个合成的默认构造函数(synthesized default constructor),反之不会合成。
默认构造函数不一定是可靠的,比如有些类成员默认初始化后的值是未定义的。如含有内置类型、复合类型成员(数组、指针)。
如果有任何成员变量不能进行默认初始化,则不会合成默认构造函数。比如类类型成员变量,没有默认构造函数。
// 默认构造函数:没有初始化列表,形参列表为空。
// 如何初始化成员:
// 1、如果有类内初始值,就执行类内初始化
// 2、否则执行默认初始化
Sales_data::Sales_data(){
}
当对象被默认初始化或值初始化时,自动调用默认构造函数。触发默认初始化的情况:
// 触发情形1(默认初始化):不适用初始值定义个非静态变量/数组。
T t;
T arr[100];
// 触发情形2(默认初始化):含有类类型成员,且该类型有合成默认构造函数
// 触发情形3(默认初始化):没有在初始值列表中显示初始化,也没有类内初始值。
触发值初始化的情况:
// 触发情形1(值初始化):初始值数量少于数组大小
T t[5] = {1, 2, 3, 4};
// 触发情形2(值初始化):没有初始化值定义一个局部静态变量
static T t; //
// 触发情形3(值初始化):显式请求值初始化
T t(......); // 如vector、string等容器。
注意!!!成员变量的初始化顺序取决于在类内的声明顺序,而不是初始值列表中的出现顺序。
struct Sales_data {
// 显示定义一个默认构造函数,等同于合成的默认构造函数。
Sales_data() = default;
Sales_data(const std::string& isbn)
: bookNo // 构造函数初始值列表
{}
Sales_data(const std::string& isbn, unsign n, double p)
: bookNo(s), units_sold(n), revenue(p*n) // 构造函数初始值列表
{}
Sales_data(std::istream&);
}
委托构造函数
delegating constructor。
class Sales_data {
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt, double price)
: bookNo(s)
, units_sold(cnt)
, revenue(cnt*price)
{
}
// 冒号后面的都是委托构造函数。
Sales_data() : Sales_data("", 0, 0) {}
Sales_data(string s) : Sales_data(s, 0, 0) {}
Sales_data(istream& is) : Sales_data(){
read(is, *this);
}
}
转换构造函数
converting constructor。只有一个形参的构造函数。
class T {
public:
T(int i);
T(std::string str);
}
void fuck(T t){
}
int main(){
T t = 1; // int 转换成T类型。
fuck(1); // int 转换成T类型。
}
但转换只能一步,如果不能一步转换到位,则表示不能转换。
class T {
public:
T(std::string str);
}
int main(){
T t = "90823"; // 错误,"90823"是字符串常量,并不是string类型。不能一次转换到位。
}
我们可以阻止这种隐式转换:
class T {
public:
explicit T(int i); // explicity关键字,将函数声明为显式的,阻止隐式转换。
}
int main(){
T t = 1; // 错误,T(int i)是explicit(显式的)
T t(1); // 正确,显式初始化。
}
拷贝、赋值、析构(待完成)
访问控制
C++的类就好比一个人,他可以为外界(类的用户)提供服务和帮助(public成员),也会有自己(类内部)的私人空间(private),还有与家人(继承)共享的空间(protected),还有与朋友(友元)共享的空间(friend)。
public&private
// 访问说明符:public、proected、private
class Sales_data {
void print(); // private,因为class的默认访问权限是private
// struct的默认访问权限是public,这是struct和class的唯一区别。
public: // 整个程序内可被访问
......
public: // 不限制数量
......
private: // 只能被类成员函数访问
......
public: // 不限制顺序
......
protected:
......
}
友元friend
class Sales_data {
// 友元声明,这些非成员函数可以访问对象的非public成员。
// 这里默认add在作用域可见,所以add的声明可以在后面
// 一般在类开头、结尾的地方集中声明
// 这不是函数声明
friend void add(const Sample&);
// 类的友元声明,同函数的友元声明
// 在FriendClass定义前,必须先声明Sample
friend class FriendClass;
// 类成员函数的友元声明,同函数友元声明。
// 在func的定义前,必须先声明Sample
friend void FriendClass::func();
}
// 除了以上的友元声明之外,还需要进行一次友元函数的声明,使友元函数对类可见。
// 这并不是普通意义上的声明,它的作用是影响访问权限,使得友元函数可在类内部可见。
Sample add(const Sample&);
std::istream &read(Sample&);
std::ostream &print(Sample&);
// 或者是extern声明,定义实现在外部。
extern Sample add(const Sample&);
extern std::istream &read(Sample&);
extern std::ostream &print(Sample&);
以下例子展示友元声明的影响:
struct X {
friend void f() { // 极端例子,友元直接定义在类内部
};
X() { f(); } // 错误,f还未被声明
void g();
void h();
}
void X::g() { f(); } // 错误,f还未被声明
void f(); // 友元函数的函数声明
void X::h() { f(); } // 这才正确了。
可变数据成员mutable
有少部分情况,需求在const常量成员函数中也能修改成员值,这就需要用到mutable特性。
class T {
Sample& fuckmutable() const {
b = 1; // 错误:const成员函数内不能修改成员
a = {"a","b","c"}; // 正确:可以修改mutable成员
return *this; // const函数,返回的是const Sample&类型
}
private:
// mutable:可变数据成员声明
mutable std:string a;
unsigned int b = 0; // 类内初始值,编译器不一定支持。
}
成员初始值
C++11标准之后,为了一个类成员设置初始值的最好做法是类内初始值。在所有构造函数中都会执行此初始化。
class T {
private:
unsigned b = 0; // 类内初始值,不一定编译器支持。
double c = 0.0;
int shit{0}; // 类内初始值,要么=,要么{}
const int ci; // 必须在构造初始值列表中初始化
int &ri; // 必须在构造初始值列表中初始化
static int sd; // 声明一个静态成员变量,保存在程序内存中的数据区
// 生命周期和程序相同。
//静态成员变量一般不要在类内初始化。
static constexpr int period = 30;
}
前向声明
struct Fucker; // Fucker类的前向声明(forward declaration)
// 在声明之后,定义之前,Fucker,知道是类,但是不知道内部情况。
Fucker *pFucker; // 不完全类型可用于指针
Fucker &refFucker; // 不完全类型可用于引用
Fucker &func(Fucker); // 不完全类型可用于函数参数,返回值
struct Sample {
Sample sample; // 错误,此时还是不完全类型。只能是指针引用、函数参数/返回值。
Sample& sample; // 正确。
}
名字查找
一般的名字查找过程:
- 在名字所在块内查找声明语句,只考虑名字使用之前出现的声明。
- 没有找到,则在外层作用域查找
- 还有没有找到匹配,就报错。
名字查找(成员声明中)
只适用于成员中使用的名字,首先会在类内查找,没找到则继续在类所在作用域查找。 ```cpp
typedef double Money; string bal; class Account { public:
// 在balance成员函数声明中使用到了Money返回类型。
// 1、在类内寻找该类型的声明,在Money使用之前,即成员声明之前,
// 2、显然没有找到,到Account类所在作用域查找,找到了Money。
Money balance() { return bal; }
private: Money bal; // … }
特殊情况:一般内层作用域的名字会覆盖外层作用域,但在类中却就有不同,如果先使用了外层的某个名字,然后再重新定义一个同名的类型,则会报错。
```cpp
typedef double Money;
class Account {
public:
Money balance() { return bal; } // 使用外层作用域的Money
double balance() { // 假如是这个成员函数,则不会触发下面的错误。
Money m = bal;
return m;
}
private:
typedef double Money; // 错误:不能重新定义Money,前面声明中已经使用了Money
// 注意是声明中,不是定义中。
Money bal;
}
名字查找(成员定义中)
成员函数定义中的名字查找:
- 在成员函数内,名字使用前查找
- 没找到,查找类内所有成员
- 特殊情况,若成员定义在函数外部,还要考虑定义之前的全局作用域中查找。
- 没有找到,在成员函数定义之前的作用域(外层作用域)查找。
针对特殊情况c:
class T {
public:
void fuck();
}
typedef double Double;
void T::fuck(){
Double d = 1.0; // 特殊情况c,在此定义之前的全局作用域中找到了Double的声明。
return d;
}
静态成员static
- 静态成员变量
- 静态成员函数 ```cpp
class Account { public: void calculate() { amount += amount * rate; } // 滚利
// 静态成员成员,不能使用this,不能const声明
static double rate() { return rate; } // 当前储蓄利率
static void rate(double); // 设置储蓄利率
private: double amount; // 当前储蓄额
static double rate; // 当前利率
static double initRate(); // 初始利率
}
// 所有静态成员不属于类对象,不能在构造函数初始化,而需要在类外部定义或初始化静态成员。 double Account::rate = initRate(); // 这是在定义和初始化rate
// 类外部定义静态成员函数 Account::rate(double r){ rate = r; }
int main(){ double r = Account::rate(); // 访问静态成员函数
Account acnt, *acnt1 = &acnt;
acnt.rate(); // 访问静态函数
acnt1->rate(); // 访问静态函数
}
<a name="fQNss"></a>
## 类内初始化
一般不会在类内初始化静态成员。满足以下条件才能类内初始化静态变量:
- 静态变量必须是字面值常量类型的constexpr
- 初始值必须是constexpr,常量表达式
```cpp
class T {
private:
static int s = 1; // 错误,s必须是constexpr类型
static const s = 1; // 正确
static constexpr int s = 1; // 正确
static string str = "1"; // 错误,str不是字面值类型constexpr
static const string str = "1"; // 错误,同上
static constexpr string str = "1"; // 错误, 同上。
}
// 如果s要类外定义,则必须按如下形式
constexpr int T::s;
成员函数引用限定符(const、&、&&)
class Foo {
public:
void fuck() &; // *this必须是一个左值引用
void fuck() &&; // *this必须是一个右值引用
void fuck() const; // *this必须是const的
Foo bitch() & const; // 错误:const限定符必须在前
Foo bitch() const &; // 正确,*this既是const的又是&的。
};
void Foo::fuck1() & { // 声明和定义中都必须有相同引用限定符。
}
void Foo::fuck2() const {
}
Foo Foo::bitch2() const & {
}
Foo a, b, &c = a;
a.fuck1(); //错误,a必须是Foo&类型
b.fuck2(); //错误;b必须是const Foo类型
c.fucke(); //正确。
final关键字
final关键字有两个用途:
- 阻止被继承
- 阻止虚函数被重写 ```cpp
class Base final { // final用途一:阻止Base被其他类继承 public:
virtual void func1() final; // final用途二:阻止虚函数func1被派生类重写
}
class Derived : public Base { // 错误:Base is final class
public:
virtual void func1() { // 错误:func1 is final class
……
}
}
``` 合理利用final可以提高性能,因为有些编译器会认为被声明为final的类是不可能再向下存在多态,完全可以优化掉查找vtable的工作(虚函数指针查找)。