龙空技术网

繁书简读之C++ Primer Day7: 类(class)

Linux后端MrWu 347

前言:

如今同学们对“类就是c语言中的结构类型”都比较注重,朋友们都想要分析一些“类就是c语言中的结构类型”的相关资讯。那么小编也在网络上收集了一些关于“类就是c语言中的结构类型””的相关文章,希望姐妹们能喜欢,你们快快来学习一下吧!

类的基本思想是数据抽象(Abstraction)和封装(Encapsulation)。数据抽象是一种依赖于接口(interface)和实现(implementation)分离的编程及设计技术。类的接口包括用户所能执行的操作;类的实现包括类的数据成员、负责接口实现的函数体以及其他私有函数

7.1 定义抽象类型7.1.1 设计sales_data类

1. 类的用户是程序员,而不是应用的使用者。设计类接口时,要考虑如何使类易于使用;当用户使用类时,不需要考虑类的实现原理

7.1.2 定义改进的sales_data类

1. 类的成员函数(member function)声明必须在类内,定义则既可以在类内也可以在类外。定义在类内部的函数是隐式的inline函数

2. 成员函数通过名为this的隐式参数来访问调用它的对象,this是一个常指针,被初始化成调用该函数的类对象的地址,在函数体内可以显示使用this指针

total.isbn();Sales_data::isbn(&total); //伪代码显示说明了total.isbn()的调用过程,即取total的地址赋给this指针std::string isbn() const { return this->bookNo; } //显示使用thisstd::string isbn() const { return bookNo; }

3. this默认是指向类类型非常量版本的常指针,所以默认不把this绑定到常量对象上,也就是说不能使用常量对象调用普通成员函数

4. 成员函数参数列表后添加const关键字代表常量成员函数,编译器处理常量成员函数调用时传入的是const this,此时,this指针同时持有指针常量和常量指针的双重属性

//isbn是const成员函数,如下伪代码展示了编译器的处理过程std::string Sales_data::isbn(const Sales_data *const this){    return this->isbn;}

5. 常量对象和指向常量对象的引用或指针都只能调用常量成员函数,常量成员函数不论声明或者定义都不要忘了参数列表之后的const关键字

6. 返回this对象的成员函数

Sales_data& Sales_data::combine(const Sales_data &rhs){    units_sold += rhs.units_sold;    revenue += rhs.revenue;    return *this; //返回调用此成员函数的对象}
7.1.3 定义类相关的非成员函数

1. 程序员通常会定义一些辅助函数,但这些函数并不属于类本身

std::ostream &print(std::ostream &os, const Sales_data &item){    os << item.isbn() << " " << item.units_sold << " "        << item.revenue << " " << item.avg_price();    return os;}
7.1.4 构造函数

1. 构造函数用来初始化类对象的数据成员,当类的对象被创建时,就会执行构造函数

2. 构造函数特点:

a) 名字与类名相同

b) 无返回类型

c) 不能是const函数

d) 可重载

3. 如果类没有显式地定义构造函数,则编译器会为类隐式地定义一个默认构造函数,该构造函数也被称为合成的默认构造函数

4. 合成默认构造函数初始化数据成员的规则:

a) 如果存在类内初始值,则用它来初始化成员(类内初始值必须以=或者{}的方式初始化

b) 否则默认初始化该成员

5. 无合成默认构造函数的场景:

a) 只有类没有声明任何构造函数时,编译器才会自动生成默认构造函数,一旦类定义了其他构造函数,除非再显式定义一个默认构造函数,否则类将没有默认构造函数

b) 如果类包含内置类型或复合类型(复合类型:数组/字符串/struct/union/enum/指针/类型组合等)成员,则仅当这些成员都存在类内初始值时,该类才适合使用合成的默认构造函数

c) 若类A中包含其他类类型成员,且该成员类没有默认构造函数,那么编译器就不会为这种类A合成默认构造函数

6. C++11及以后,可以在函数列表后添加=default来要求编译器生成构造函数,如果=default出现在类内部,则默认构造函数是内联的

7. 构造函数初始值列表:

Sales_data(const std::string &s, unsigned n, double p):				bookNo(s), units_sold(n), revenue(p*n) { }

8. 当某些数据成员被构造函数初始值列表忽略时,它们会以与合成默认构造函数相同的方式隐式初始化

Sales_data(const std::string &s):			bookNo(s), units_sold(0), revenue(0) { }
7.1.5 拷贝/赋值和析构

编译器能合成拷贝、赋值和析构函数,但是对于某些类来说合成的版本无法正常工作。特别是当类需要分配类对象之外的动态内存,合成的版本通常会失效

7.2 访问控制与封装

1. 使用访问说明符可以加强类的封装性:

a) public:关键字之后的成员在整个程序生命周期内都可以访问

b) private:关键字之后的成员只能被类的成员函数访问,private隐藏了类的实现细节

2. 使用struct定义类时,默认所有成员都时public属性;使用class时,默认所有成员都是private

7.2.1 友元

1. 类可以允许其他类或函数访问它的非公有成员,方法是使用关键字friend将其他类或函数声明为它的友元

class Sales_data{    //类的友元函数声明    friend Sales_data add(const Sales_data&, const Sales_data&);    friend std::istream &read(std::istream&, Sales_data&);    friend std::ostream &print(std::ostream&, const Sales_data&);public:    Sales_data() = default;    Sales_data(const std::string &s, unsigned n, double p):        bookNo(s), units_sold(n), revenue(p*n) { }    Sales_data(const std::string &s): bookNo(s) { }    Sales_data(std::istream&);    std::string isbn() const { return bookNo; }    Sales_data &combine(const Sales_data&);private:    std::string bookNo;    unsigned units_sold = 0;    double revenue = 0.0;};// 类外友元函数声明Sales_data add(const Sales_data&, const Sales_data&);std::istream &read(std::istream&, Sales_data&);std::ostream &print(std::ostream&, const Sales_data&);

2. 友元必须定义在类内,友元不是类的成员,不受区域访问级别限制,通常在类定义开始或结束前集中声明友元

3. 为了使友元对类的用户可见,通常会把友元的声明(类的外部)与类本身放在同一个头文件中(注意:为了使用友元函数,类内类外的声明都必须存在,类内只是声明了这些函数的权限,而类外不带friend关键字的函数才是真正声明)

7.3 类的其他特性7.3.1 类成员再探

1. 类可以自定义某种类型在类内的别名,类型成员一样有访问级别限制

class Screen{public:    using pos = std::string::size_type;};

2. 与普通成员不同,用来定义类型的成员必须先定义后使用

3. 内联成员函数:

a) 在类内定义函数,为隐式内联

b) 在类内用inline显示声明成员函数

c) 在类外用inline定义成员函数

d) 同时在类内类外修饰

4. 使用关键字mutable可以声明可变数据成员,可变数据成员永远不会是const的,即使它在const对象内。因此const成员函数可以修改可变成员的值

class Screen{public:    void func() const;private:    mutable size_t mut_val; //即使在const对象内也可能改变};void Screen::func() const{    ++mut_val;}
7.3.2 返回*this的成员函数

1. const成员函数如果返回*this,则返回类型为常量引用

2. 通过区分成员函数是否是const,可以对其重载;对于常量对象,只能调用const版本函数,非常量对象则优先调用非常量版本

class Screen{public:    //根据是否为const实现重载    Screen &display(std::ostream &os)    { do_display(os); return *this; }    const Screen &display(std::ostream &os) const    { do_display(os); return *this; }private:    void do_display(std::ostream &os) const    { os << contents; }};Screen myScreen(5,3);const Screen blank(5, 3);myScreen.display(cout); //调用非常量版本blank.display(cout); //调用常量版本
7.3.3 类类型

1. 可以仅仅声明一个类而暂时不定义它,这种声明被称作前向声明(forward declaration),在类声明之后定义之前它都是一个不完全类型(incomplete type)

2. 可以定义指向不完全类型的指针或引用,也可以声明(不能定义)以不完全类型作为参数或返回类型的函数

3. 只有当类全部完成后才算被定义,所以一个类的成员类型不能是该类本身。但是一旦类的名字出现,就可以被认为是声明过了,因此类可以包含指向它自身类型的引用或指针

//我们常见的链表声明class LinkList{    int value;    LinkList *next;    LinkList *prev;};
7.3.4 友元再探

1. 除了普通函数,还可以把其他类或其他类的成员函数声明为友元。友元类的成员函数可以访问此类包括非公有成员在内的所有成员

2. 友元函数可以直接定义在类的内部,这种函数是隐式内联的。但是必须在类外部提供相应声明令函数可见

struct X{    friend void f() { /* friend function can be defined in the class body */ }    X() { f(); } // error: no declaration for f    void g();    void h();};void X::g() { return f(); } // error: f此时还没有被声明void f(); //声明类X中声明的友元函数void X::h() { return f(); } // ok: 因为f已经被生命过

3. 友元关系不能传递

4. 把其他类的成员函数声明为友元时,必须明确指定该函数所属的类名

class Screen{     // Window::clear必须已经声明过    friend void Window::clear();};

5. 如果类想把一组重载函数声明为友元,需要对这组函数中的每一个分别声明

7.4 类的作用域

1. 当类的成员函数的返回类型也是类的成员时,在定义时要指明类

Student::age Student::Getage(){}
7.4.1 名字查找与类的作用域

1. 类的定义过程:

a) 编译成员的声明

b) 直到全部类可见后才编译函数体

2. 注意:

a) 在类内定义的类型别名要放在类的开始,放在后面其他成员是看不见的

b) 类外已定义的类型名不能在类内重新定义

3. 成员函数中名字的解析顺序

a) 在成员函数内查找该名字的声明,只有在函数使用之前出现的声明才会被考虑

b) 如果在成员函数内没有找到,则会在类内继续查找,这时会考虑类的所有成员

c) 如果类内也没有找到,会在成员函数定义之前的作用域查找

4. 通常尽量避免将一个变量和类成员变量定义成重名,另外,可以通过作用域运算符或this指针来强制访问被隐藏的类成员

void Screen::dummy_fcn(pos height){    cursor = width * this->height;//强制使用成员变量    // 另外一种写法    cursor = width * Screen::height; //强制使用成员变量    //以上两个表达式如果不用::或this,那么height就会使用形参接受的值}
7.5 构造函数再探7.5.1 构造函数初始值列表

1. 使用初始值列表对类成员的初始化才是真正的初始化,在构造函数的函数体内赋值并不是初始化

2. 如果成员是const或者引用,再或者是某种未定义默认构造函数的类类型,必须在初始值列表中将它们初始化

3. 如果一个构造函数为所有参数提供了默认实参,则它实际上相当于定义了默认构造函数

7.5.2 委托构造函数

1. 委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程

class Student{public:    Student(std::string name, int age):_name(name), _age(age) { }    Student():Student(“ ”, 18) { } //这就是委托构造函数    Student(std::string):Student(s, 18) { }//这也是委托构造函数};
7.5.3 默认构造函数的作用

1. 当对象被默认初始化或值初始化时会自动执行默认构造函数

2. 默认初始化的情况:

a) 在块作用域内不使用初始值定义非静态变量或数组

b) 类本身含有类类型的成员且使用合成默认构造函数

c) 类类型的成员没有在构造函数初始值列表中显式初始化

3. 值初始化的情况:

a) 数组初始化时提供的初始值数量少于数组大小

b) 不使用初始值定义局部静态变量

c) 通过T()形式(T为类型)的表达式显式地请求值初始化

4. 实际使用中,即便定义了其他构造函数,最好也提供一个默认构造函数

7.5.4 隐式的类类型转换

1. 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制。这种构造函数被称为转换构造函数

2. 一个实参的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则

3. 执行隐式转换时,编译器只会自动执行一步类型转换

std::string null_book = “999-9”;item.combine(null_book); //ok, combine函数接受Sales_data类类型,但该类定义了一个接受string参数的转换构造函数,所以这里会执行从string到该类类型的隐式转换item.combine(“999-9”); //error, 隐式使用了两次转换,所以错误item.combine(std::string(“999-9”); //ok,先显示转换,再执行一次隐式转换

4. 将转换构造函数声明为explicit将会阻止隐式转换

5. 关键字explicit只对一个实参的构造函数有效,因为需要多个实参的构造函数不执行隐式转换,且explicit关键字只用在类内声明函数时

class Sales_data{public:    explicit Sales_data(const std::string &s):book(s) { } //禁止隐式转换private:    std::string bookNo;};iter.combine(null_book); //error,不能执行从std::string到Sales_data的隐式初始化

6. 可以使用explicit的构造函数显示地强制进行转换

iter.combine(static_cast<Sales_data>(null_book));//ok,static_cast可以使用explicit的构造函数
7.5.5 聚合类

1. 条件:

a) 所有成员都是public

b) 没有定义构造函数

c) 没有类内初始值

d) 没有基类

e) 没有虚函数

2. 可以使用花括号包围的成员初始值列表初始化聚合类。初始值顺序必须与声明顺序一致。如果初始值列表中的元素个数少于类的成员个数,则靠后的成员被值初始化

7.5.6 字面值常量类

1. 数据成员都是字面值类型的聚合类是字面值常量类

2. 或者不是聚合类,但满足下列条件,也是字面值常量类:

a) 数据成员都是字面值类型

b) 至少含有一个constexpr构造函数

c) 如果数据成员含有类内初始值,则内置类型成员的初始值必须是常量表达式;如果成员属于类类型,则初始值必须使用成员自己的constexpr构造函数

d) 类必须使用析构函数的默认定义

3. 类的构造函数不能是const的,但是字面值常量类的构造函数可以是constexpr函数,constexpr构造函数必须初始化所有数据成员,初始值使用constexpr构造函数或常量表达式,且函数体为空(因为constexpr函数体只能包含一条返回语句,但是构造函数不能包含返回语句)

4. 字面值常量类于是字面值类型

5. constexpr函数的参数和返回值必须都是字面值类型

class Debug{public:    constexpr Debug(bool b=true):a(b) { }private:    bool a;};constexpr Debug prod{false};//定义对象,实参应为常量表达式
7.6 类的静态成员

1. 使用关键字static可以声明类的静态成员。静态成员存在于任何对象之外,对象中不包含与静态成员相关的数据

2. 由于静态成员不与任何对象绑定,因此静态成员函数不能声明为const的,也不能在静态成员函数内使用this指针(所以类的成员函数不能同时用static和尾后const修饰),也因此,static函数无法访问非static的数据成员

3. 用户代码可以使用作用域运算符访问静态成员,也可以通过类对象、引用或指针访问。类的成员函数可以直接访问静态成员

4. 在类外部定义静态成员时,不能重复static关键字,其只能用于类内部的声明语句,一个静态成员只能被定义一次,且存在于程序的整个生命周期

5. 建议把静态成员定义和类的非内联函数定义放在同一源文件中

6. 通常情况下,不应该在类内部初始化静态成员,但可以为静态成员提供const整数类型的类内初始值,不过要求静态成员必须是字面值常量类型的constexpr,初始值必须是常量表达式,而且同样需要在类外定义

class Account{public:    static double rate() { return rt; }    static void rate(double);private:    static constexpr int period = 30; // 常量定义    double tbl[period];};

7. 静态成员使用的特殊场景:

a) 静态数据成员的类型可以时它所属的类类型

class Account{    static Account acc;};

b) 可以使用静态成员作为函数的默认实参

class Screen{public:    Screen &clear(char = background);private:    static const char background;};

标签: #类就是c语言中的结构类型 #类是c语言中的结构类型吗