《C++ Primer》阅读笔记之第七章——类

本文详细探讨了C++中的类,包括常量成员函数、构造函数及其相关概念,如构造函数初始化列表、访问控制与封装、静态成员等。重点讲解了构造函数不能声明为常量函数、初始化顺序、隐式类型转换以及explicit关键字的作用,强调了列表初始化的效率优势和静态成员的特殊性。
摘要由CSDN通过智能技术生成

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)constconst定义了常量函数,其具体作用是修改了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;
};

总结:
列表初始化执行的是初始化操作,而构造函数内执行的是赋值操作。
初始化值选择:

  1. 若有列表初始值,则使用列表初始化赋予的值。
  2. 没有列表初始值,则使用类内初始值。
  3. 没有类内初始值,则为默认初始值。
    列表初始化会减少一次默认初始化构造函数的调用,因此推荐使用列表初始化。

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)); // 正确,强制类型转换
}

注意:

  1. explicit关键字只对一个参数的构造函数有效,多个参数无需无法进行隐式转换,因此无需添加该关键字。
  2. explicit关键字只允许在类内声明使用,类外定义时不能重复添加。

3. 访问控制与封装

1. class与struct的不同

classstruct的唯一不同之处在于默认权限,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. 静态成员变量的初始化

静态成员变量不属于类,因此不是由构造函数初始化的。一般而言不能在类内初始化,而应该在类外初始化。constconstexpr常量静态变量可以直接在类内初始化而无需在类外定义,但是任何对其类型的微小改变都会出现错误。

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;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值