目录
7.1 定义抽象数据类型
const成员函数和this指针
返回this对象
构造函数的性质
合成的默认构造函数
默认构造函数和default
初始化列表
7.2 访问控制与封装
友元
友元的声明
声明友元不等于声明其函数(友元的作用域)
7.3 类的其它特性装
内联函数的声明
返回*this的成员函数
基于const重载函数
编程习惯!
7.4 类的作用域
定义在类外部的成员
编程习惯!
7.5 构造函数再探
构造函数初始化的顺序
关于默认构造函数
隐式的类类型转换
阻止:隐式的类类型转换
7.6 类的静态成员
静态成员
静态成员的初始化
const
成员函数和this
指针this
指针,在默认情况下,是类类型非常量版本的常量指针,例如下面的Person
类的this
指针默认是Person *const this
。
意思就是:默认情况下,不能修改this
指针指向的对象是谁,但可以通过this
指针修改指向对象的值。
然而,如果有一个成员变量是const
类型,例如下面的const int m_a
,而成员函数func
要用到这个成员变量,但又不希望修改它,因此需要在函数后面加一个const
:
class Person
{
public:
void func(int a) const
{
this->m_a = a; // 错误,因为上面声明了const,因此this指向的成员不可修改
cout << m_a;
}
const int m_a = 10;
};
void test()
{
Person p;
p.func(20);
}
this
对象class Person
{
public:
Person& add(Person p) // 返回Person引用,也对应了 return *this;
{
this->Age += p.Age;
return *this;
}
int Age = 10;
};
void test()
{
Person p1, p2;
p1.Age = 10;
p2.add(p1);
cout << p2.Age;
}
return *this
返回的就是当前这个对象,例如p2.add(p1)
,那么这个this
指的是p2
这个对象,因此第6-7
行等价于:p2.Age += p1.Age; return &p2;
或者说,add()
成员函数的返回值是调用add()
的对象的引用,第16
行是p2
调用add
,因此add()
返回的那个*this
就是p2
的引用。通过输出地址,可以发现,this
的地址和p2
的地址是同一个。
const
型。当声明一个const
对象时,需要通过构造函数向其写值(初始化过程),只有在初始化完成之后,这个对象才具有const
属性。概念:如果我们没有显式地写一个构造函数,那么编译器会帮我们自动创建一个隐式的构造函数,就叫做“合成的默认构造函数”。
default
显式补充默认构造函数,不然的话,有了其它构造函数后,就会丢失默认构造函数。
class Person
{
public:
Person() = default; // 显式定义一个默认构造函数
// Person() {}; // 和上面等价,但只能二选一
Person(int age, int id, string name) : Age(age), Id(id), Name(name) {}; // 自己定义其它构造函数
int Age = 10;
int Id = 20;
string Name = "wind";
};
void test()
{
Person p1(20, 101, "wind");
Person p2; // 调用第4行的默认构造函数
}
如果没有第4行,则第15
行就出错,因为我们已经在第5
行定义了一个构造函数,所以编译器就不会自动创建一个合成的默认构造函数!因此,需要自己加上一个默认构造。
格式如下:
class Person
{
public:
// 初始化列表格式:
Person(int age, int id, string name) : Age(age), Id(id), Name(name) {};
int Age = 10;
int Id = 20;
string Name = "wind";
};
作用:其它类或者函数能够访问类内部私有成员。
注意:
public
还是private
都行,不过建议在类定义开始或结束前集中声明。class Person
{
public:
Person() = default;
Person(int age, int id, string name) : Age(age), Id(id), Name(name) {};
friend void test(int a); // friend + 函数声明(要和函数声明一模一样)
private:
int Age = 10;
int Id = 20;
string Name = "wind";
};
void test(int a)
{
Person p1(20, 101, "wind");
Person p2;
cout << p2.Age; // 如果test函数不是友元,那么这句就报错,因为无法访问私有成员Age
}
因为友元只是制定了访问权限,并不是真正的函数声明,如果其它函数需要调用这个友元函数,那么必须在专门针对这个函数做一个声明。通常情况,把针对友元的声明和类本身放在同一个头文件中。
要注意几点:
class Student{public:void func();};
class Person
{
friend void Student::func(); // 必须有Student::
}
class Student
{
public:
void func();
void func(int a);
};
class Person
{
friend void Student::func();
friend void Student::func(int a); // 如果不写这个,那么这个版本的func就无法访问Student的私有成员
}
inline
(隐式内联)inline
的即使const
,也能修改变量值:mutable
关键字:mutable
作用:把一个变量变成“可变成员”
class Person
{
public:
void func(int a) const
{
this->m_a = a; // 正确,即使函数是const, 但m_a是mutable的
this->m_b = a; // 错误,因为m_b不是mutable, 因此不可被修改
cout << m_a;
}
mutable int m_a = 10;
int m_b = 10;
};
*this
的成员函数一大作用:可以把一系列操作串起来。
伪代码:
Person& add(int a)
{
...
return *this;
}
Person& sub(int a)
{
...
return *this;
}
Person p;
p.add(1).sub(2); // 串起来操作,意思是先加1,再减2
注意:针对返回的函数定义为const
,那么返回的*this
也会变成const
对象。
const
重载函数直接看例子:
class Person
{
public:
Person() = default;
Person(int num) : Num(num) {};
// 非const重载的display()
Person& display()
{
cout << "非const: " << this->Num << endl;
return *this; // 返回非const对象
}
// const重载的display()
const Person& display() const
{
cout << "const: " << this->Num << endl;
return *this; // 返回const对象
}
Person& add(int a)
{
this->Num += a;
return *this;
}
int Num;
};
void test()
{
Person p1(10); // 非const对象
const Person p2(20); // const对象
p1.display(); // 调用非const的display()
p1.add(10).display(); // 调用非const的display()
p2.display(); // 调用const的display()
}
如果没有写非const
重载的display()
,那么最后p1
就会调用const
重载的display()
,对应《C++ Primer》第248页最上面:
如果让add
变成const
,Num
变成mutable
,那么p1.add(10).display()
就会调用const
的display()
,因为此时p1.add(10)
返回的是const
对象,自然会首选const
的display()
。
对于公用代码使用私有功能函数,就像上面为输出结果而专门写的小函数display()
,原因如下:
inline
,因此后面调用它的时候就不会带来额外的运行开销。《C++ Primer》第243
页讲到定义一个类型成员:
也就意味着,这个pos
的作用域就是类内部,如果在外面访问,需要加上Person::
表面其作用域;同理,类内声明的函数也是如此:
class Person
{
public:
Person() = default;
Person(int num) : Num(num) {};
using type1 = int; // 定义类型
type1 func(type1 a); // 声明函数
};
Person::type1 Person::func(Person::type1 a)
{
cout << a;
}
重点在第11
行:
如果type1
前不加Person::
,那么会报错"未定义标识符"。
如果func
前不加Person::
,那么这里定义的func
就不是第8
行声明的那个func
,而是一个新的函数,返回类型是type1
,也就是int
,等价于:int func(int a){cout << a;}
,和Person
完全无关。
如果保持11
行的代码,那么调用int a = p1.func(1);
的时候,就会报错,因为func
重定义了。
即,把类似using xxx = xxx;
和typedef xxx yyy;
放在类的开始处。
成员的初始化顺序与它们在类定义中的出现顺序一致。
class Person
{
public:
Person() = default;
int Age;
int Id;
Person(int val) : Id(val), Age(Id) {}; // 错误
Person(int val) : Age(val), Id(Age) {}; // 正确
};
void test()
{
Person p1(10);
cout << p1.Age << endl << p1.Id << endl;
}
Age
先定义,Id
后定义,因此:
第8
行是正确的:先用初始化Age
--使用val
,再初始化Id
--使用Age
;
第7
行是错误的:先用初始化Age
--使用Id
,而此时Id
还没有被初始化,因此输出p1.Id
是个乱码。
注:应该尽量按顺序初始化,并且不要用某些成员去初始化其它成员(例如用Id
去初始化Age
)。
如果类A没有默认构造函数,类B有一个类A的成员,那么想要定义类B的默认构造函数,就必须显式调用A的带参构造函数初始化这个类A的成员:
class Person
{
public:
//Person() = default;
Person(int a) {}; // 只有一个带参构造函数
int Val = 10;
};
class C
{
public:
Person p;
C(int i = 0) : p(i) {}; // 写C的默认构造的时候,必须显式调用A的带参构造函数初始化成员p
};
void test()
{
C c;
cout << c.p.Val;
}
如果第13
行改为:C(int i)
,那会第18
行报错,提示不存在默认构造函数。也就是说,Person
没有默认构造,因此无法用(int i) : p(i)
的方式去初始化p
,只能用值初始化的方式。
如果第4
行存在,那么Person
就有默认构造函数了,从而可以省略第13
行。
大前提:只有“具有1个实参”的构造函数能够隐式转换,没有实参、有多个实参的构造函数都不行。
例如,下面第21-22
行,就是通过实参调用构造函数,然后就会把这个int
型转换为Person
型。实际上是通过创建一个临时Person
对象来过渡的:
class Person
{
public:
Person() = default;
Person(int a) {};
Person& add(const Person& p)
{
this->Val += p.Val;
return *this;
}
int Val = 10;
};
void test()
{
Person p1(10), p2;
int a = 10;
p2.add(a);
p2.add(int(10));
cout << p2.Val;
}
第20-21行等价于:
int a = 10;
Person temp(a); --> 临时对象
p2.add(temp);
关键词:explicit
class Person
{
public:
Person() = default;
explicit Person(int a) {};
...
};
void test()
{
Person p1(10), p2;
int a = 10;
p2.add(a); // 错误!因为第5行声明了explicit,不再允许!
cout << p2.Val;
}
注1:explicit
只允许出现在类内的构造函数声明处,类外不行!
注2:explicit
只是防止隐式转换,如果要强制转换,explicit
就没有用了:
class Person
{
public:
Person() = default;
explicit Person(int a) {};
...
};
void test()
{
Person p1(10), p2;
int a = 10;
p2.add(static_cast(a)); --> 对其强制转换
cout << p2.Val;
}
关键字:static
主要为了调用方便,不需要生成对象就能调用:
class X
{
public:
void MethodA();
static void MethodB();
}
此时MethodB
可以直接调用,X::MethodB();
MethodA
必须先生成类对象才能调用,X x; x.MethodA();
注意:
this
指针。const
。static
,所以生命周期是持续到程序结束。static
关键字只能在类内部,如果在类外部定义静态成员,那么不能重复static
关键字:class Person
{
public:
static Person& add(const Person& p)
{
this->Val += p.Val; // 错误,因为声明为static后,不包含this指针
return *this;
}
static void func();
int Val = 10;
};
static void Person::func() {} // 错误,不能在类外重复关键字static
void Person::func() {} // 正确
const
或constexpr
。class Person
{
public:
static const int a = 10; // 正确,a是个静态常量成员
static vectorv(a); // 错误
static vectorv; // 正确,必须在类外初始化
};
class Person
{
public:
static person p1; // 正确,静态数据成员可以是非完全类型
Person *p2; // 正确,指针类型也可以是非完全类型
Person p3; // 错误,非静态数据成员必须是完全类型
}
小结:只要想到“不和任何对象绑定在一起”,再能理解这些了。