C++ Primer 第七章 类
1. 常量成员函数
默认情况下,this
的类型是指向类类型非常量版本的常量指针,即this
指针本身不可更改(顶层指针),但其指向的内容可更改。
假设类Book
, 则this
的声明为Book *const this;
因此不能将常量变量赋予this
, 所以若定义了一个常量对象,则该对象无法访问非常量函数:
class Book{
public:
string name;
Book(string _name){
name = _name;
}
string getname(){
return name;
}
};
const Book book("name");
book.getname(); // error,常量对象无法访问非常量函数
class Book{
public:
string name;
Book(string _name){
name = _name;
}
string getname() const {
return name;
}
};
const Book book("name");
book.getname(); // 正确,常量对象访问常量函数
总结:
Book(string _name)const
中const
定义了常量函数,其具体作用是修改了this的默认类型,this默认为顶层const,无法修改本身内容,在常量函数中,添加了底层const属性,也无法修改其指向的内容。即常量函数中不允许修改类的成员变量。因此若不想让一个函数修改成员变量,可以使用常量函数。
非常量对象可以调用常量和非常量方法,但常量对象只能调用常量方法。
2. 构造函数
1. 关于常量构造函数的问题
构造函数不能申明为常量函数。同时,构造函数即使为非常量函数,创建常量对象时仍能调用非常量构造函数。这是因为,在构造函数运行期间,为初始化过程,此时对象并未取得常量属性,构造完成后,对象才为常量对象。
2. 内联函数
定义在类内的函数默认添加inline属性,但如同普通的inline函数一样,该函数是否内联最终由编译器决定,这里只是提出建议。
public:
string name;
Book(string _name){
name = _name;
}
// 该函数默认有inline属性
string getname() const {
return name;
}
};
const Book book("name");
book.getname(); // 正确,常量对象访问常量函数
3. 默认构造函数
在构造函数的声明后添加= default
则该构造函数与编译器自动生成的默认构造函数完全一样。同样,若= default
出现在类内,则默认是内联,否则默认非内联。
class Book{
Book() = default;
};
4. 构造函数初始化列表
为类中的成员变量初始化。
class Book{
string name = "book1";
int id = 1;
// id初始化为1
Book(string _name): name(_name){};
};
上面的构造函数通过初始化列表为成员函数初始化,对于列表没有包括的成员变量,则使用类内初始值进行初始化。
所谓类内初始值,即成员变量定义时赋予的初始值。如id
没有显示初始化,则使用1
进行初始化。若类内初始值也没有,则默认初始化。
注意:赋予类内初始值时只能通过=
或{}
,而不能用()
。因为使用()
无法和函数声明区分。
class Book{
string name = "book";
int id{1};
typedef int x;
int y(x); // 这样就成了函数声明
};
这种方式相比于构造函数内初始化而言,若参数为基本数据类型,则结果相同;对于自定义类型而言,初始化列表初始化会调用一次类的拷贝构造函数,而构造函数内初始化会调用一次默认初始化构造函数,一次赋值构造函数,这是因为编译器会确保在执行构造函数之前,所有成员变量均以初始化,因此会先对成员变量进行一次默认初始化。
class A {
public:
A() {
cout << "A Default constructor" << endl;
}
A(const A&) {
cout << "A copy constructor" << endl;
}
A& operator=(const A& a) {
cout << "A assign operator" << endl;
return *this;
}
};
class B {
public:
B() = default;
// 在这里调用一次拷贝构造函数
B(A &a): _a(a){}
private:
A _a;
};
class B {
public:
B() = default;
// 进入构造函数前_a还未初始化,因此会进行默认初始化
B(A &a){
// 这里调用一次赋值构造函数
_a = a;
}
private:
A _a;
};
总结:
列表初始化执行的是初始化操作,而构造函数内执行的是赋值操作。
初始化值选择:
- 若有列表初始值,则使用列表初始化赋予的值。
- 没有列表初始值,则使用类内初始值。
- 没有类内初始值,则为默认初始值。
列表初始化会减少一次默认初始化构造函数的调用,因此推荐使用列表初始化。
5. 初始化顺序
使用列表初始化时,变量的初始化顺序取决于其定义的顺序,而与列表顺序无关。
class A{
public:
int x;
int y;
// x先被初始化,此时y还被初始化,因此值不确定
A(): y(1), x(y){}
};
6. 委托构造函数
委托构造函数即一个构造函数“委托”同一个类的另一个构造函数进行初始化。
class A{
public:
int x, y;
A(int _x, int _y): x(_x), y(_y){}
// 以下两个函数均委托上面的构造函数进行初始化
A(int _x): A(_x, 0){}
A(): A(0, 0){}
};
7. 隐式类类型转换
当类的构造函数只有一个参数时,则其实际上定义了转换为此类类型的隐式类型转换机制,也称为转换构造函数。
class A{
public:
string x;
A(string _x): x(_x){}
string add(const A& a){
x += a.x;
return x;
}
};
int main(){
A a("x");
string y = "y";
cout << a.add(y) << endl;
}
编译器构造了一个临时变量并传入到add
函数中,因此add
函数的参数a
不能是非常量引用,而只能是常量引用或非引用。
但是编译器只允许一步转换,下面例子会报错,需要两步转换:char[] -> string -> A
。
int main(){
A a("x");
cout << a.add("y") << endl; // 错误
}
8. explicit关键字
将函数声明为explicit类型即阻止了隐式通过该构造函数创建对象,因此可以阻止隐式转换的发生:
class A{
public:
string x;
explicit A(string _x): x(_x){}
string add(const A& a){
x += a.x;
return x;
}
};
int main(){
A a("x");
// 错误,无法进行隐式转换
cout << a.add(string("y")) << endl;
}
通过=
调用赋值构造函数时,也可能会发生隐式转换,因此对于explicit构造函数,只能直接初始化。
int main(){
string x = "x";
A a(x); // 正确
A b = x; // 错误,无法隐式转换
}
可以通过强制类型转换使用explicit构造函数。
int main(){
string x = "x";
A a("y");
a.add(static_cast<A>(x)); // 正确,强制类型转换
}
注意:
- explicit关键字只对一个参数的构造函数有效,多个参数无需无法进行隐式转换,因此无需添加该关键字。
- explicit关键字只允许在类内声明使用,类外定义时不能重复添加。
3. 访问控制与封装
1. class与struct的不同
class
和struct
的唯一不同之处在于默认权限,class
默认权限为private
,而struct
的默认权限为public
。
2. 友元
友元允许其他类或者函数访问一个类的非公有成员。定义友元只需要在声明前添加friend
关键字即可,并且友元的声明必须在类内部,不受权限控制符的控制,一般定义在开头或结尾。
class A{
// 定义友元函数
friend void add(A &a, int _x);
// 定义友元类
friend class B;
private:
int x;
};
注意:
部分编译器实现中,友元声明只是表明控制权限,并不是一般意义上的声明,因此需要在类外再次进行声明或定义。
4. 类的其他特性
1. 可变数据成员
可变数据类型永远不会是const的,即使位于const对象内或const成员函数内。通过mutable
关键字来定义可变数据成员。
class A{
// 定义可变数据成员
mutable int cnt;
void print() const {
cnt++; // 仍然可以修改
}
};
5. 类的作用域
1. 名字查找
类中编译完所有声明在编译函数体,因此函数体内可以使用在任何地方声明的成员。
但类型必须在之前定义,一般而言类型定义在类的开头。
class A{
public:
MY_TYPE f(){ // 错误, 此时MY_TYPE尚未定义
return x; // x可以使用
}
int x = 0;
typedef int MY_TYPE;
};
另外,类内成员可以隐藏类外的同名声明,但对于类型定义而言,若该类型已在类内使用,则不能重新定义。
class A{
public:
MY_TYPE x;
typedef int MY_TYPE; // 错误,即使MY_TYPE仍是同一类型
};
class A{
public:
typedef int MY_TYPE; // 正确
MY_TYPE x;
};
总结:
类型定义最好总是定义在类的开头。
2. 同名声明的使用
使用::
引用全局作用域变量
int x;
class A{
public:
int x;
void fun(int x){
x = 1; // 函数参数
this->x = 1; // 类成员变量
::x = 1; // 全局变量
}
};
6. 聚合类
满足以下条件的类为聚合类:
- 所有成员都是public
- 没有定义任何构造函数
- 没有类内初始值
- 没有基类
- 没有virtual函数
struct Data{
int x;
string s;
};
Data
即为一个聚合类,聚合类可以通过花括号初始化:
Data d = {1, "x"};
初始化必须按照定义顺序,且参数不能多于成员变量个数。
7. 字面值常量类
数据成员都是字面值类型的聚合类为字面值常量类,或者满足以下条件的非聚合类也为字面值常量类:
- 数据成员都是字面值。
- 类中至少有一个
constexpr
构造函数。 - 若数据成员含有类内初始值,对于内置数据类型,初始值必须是常量表达式;对于类类型,初始值必须使用成员自己的
constexpr
构造函数。 - 必须使用默认析构函数。
构造函数不能是const
,但在字面值常量类中可以是constexpr
。
字面值常量类中所有数据成员都必须初始化,通过类内初始值或构造函数。
class Debug{
public:
constexpr Debug(bool b=true):hw(b),io(b),other(b) {}
constexpr Debug(bool h,bool i,bool o):hw(h),io(i),other(o) {}
constexpr bool any() {return hw||io||other;}
void set_io(bool b) {io=b;}
void set_hw(bool b) {hw=b;}
void set_other(bool b) {hw=b;}
private:
bool hw;
bool io;
bool other;
};
constexpr Debug io_sub(false, true, false);
io_sub.any();
8. 静态成员
1. 静态成员的定义
静态成员属于类,而不属于对象,因此在静态成员函数中不能使用this
指针,因而也不能定义静态常量函数,但是可以定义静态常量成员变量。
与explicit
类似,static
关键字只能在类内使用,在类外定义时不能添加static
。
2. 静态成员的调用
静态成员可以通过对象或类调用。
3. 静态成员变量的初始化
静态成员变量不属于类,因此不是由构造函数初始化的。一般而言不能在类内初始化,而应该在类外初始化。const
或constexpr
常量静态变量可以直接在类内初始化而无需在类外定义,但是任何对其类型的微小改变都会出现错误。
class A{
public:
static int x;
static const int y = 1;
static constexpr int z1 = 1;
static constexpr int z2 = 1;
static int add(int _a);
};
// 类外初始化
int A::x = 1;
constexpr int A::z2; // 不能再定义初值
int A::add(int _a){
x += _a;
}
void test(const int& a){}
int main(){
A a;
cout << A::x << endl; // 类名调用,1
a.add(1);
cout << a.x << endl; // 对象调用,2
test(A::z1); // 编译错误
test(A::z2); // 正确,z2在类外定义了
}
总结:
最好所有的静态成员变量都在类外定义,对于有类内初值的常量静态变量,类外定义时不能再定义初值。
4. 特殊场景
静态成员变量可以用于某些不能使用普通成员变量的地方。
- 作为自身类的成员
静态成员可以是不完全类型,即已声明还未定义的定义。
class A{
static A a;
A a; // 错误
};
- 作为参数的默认值
class A{
int a = 0;
static int b;
// 错误,不能使用非静态成员作为默认值
void test1(int x = a){}
// 正确
void test2(int x = b){}
};
int A::b = 1;