[C++]类基础

Posted jiangwei0512

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[C++]类基础相关的知识,希望对你有一定的参考价值。

基础

类用struct或者class来定义。两者的区别是struct里面未明确声明访问权限的成员默认是public的,而class默认是private的。由于这个差异,所以下面的代码是可行的:

struct People {
    string name;
    int age;
    void hello() {
        cout << "hello, my name is " + this->name << endl;
    }
};

int main()
{
    People p;
    p.hello();

    return 0;
}

但是如果将struct改成class就会报错:

Classes.cc:39:13: error: 'void People::hello()' is private within this context
     p.hello();
             ^
Classes.cc:31:10: note: declared private here
     void hello() {
          ^~~~~
make: *** [../GNU_makefile_template:42: Classes.o] Error 1

这时需要用上访问说明符来明确hello()是public的:

class People {
    string name;
    int age;
public:
    void hello() {
        cout << "hello, my name is " + this->name << endl;
    }
};

这里的hello()定义在函数内部,它是隐式的inline函数。类的成员函数也可以定义在外部,下面是一个例子:

class People {
    string name;
    int age;
public:
    void hello() const {
        cout << "hello, my name is " + this->name << endl;
    }
    void setName(string);
};

inline void People::setName(string s) {
    this->name = s;
}

外部定义的类函数也可以设置为内联函数,这需要显式地使用inline关键字。这里使用的this表示的是当前对象的指针。为了访问它自己的成员,就可以使用this->。由于hello函数只是打印名字,并不会也不应该更改名字,所以这里我们可以将它设置为常量成员函数,就是在函数形参列表之后函数体之前加上const:

public:
    void hello() const {
        // this->name = "Jack"; // 错误
        cout << "hello, my name is " + this->name << endl;
    }

这里的const实际上是修饰this指针的,这样就不能通过它修改对象的成员了。但是如果想在常量成员函数中也修改数据成员,也不是不可以,此时,需要在该数据成员声明中增加mutable关键字:

mutable string name;

常量成员函数中的this是指向const的指针,而*this是const对象。如果以引用的形式返回*this,那么它的返回类型将是常量引用。

构造函数

构造函数的名字和类型相同。如果没有提供构造函数,则创建对象时使用默认构造函数,它由编译器生成,又被称为合成的默认构造函数。默认构造函数初始化成员的方式:

  • 如果存在类内初始值,用它来初始化成员。

  • 否则,默认初始化该成员。

下面是一个示例:

class People {
    string name;
    int age = 18;
public:
    void hello() const {
        cout << "hello, my name is " + this->name << endl;
        cout << "I am " + to_string(this->age) + " years old" << endl;
    }
    void setName(string);
};

这里的age有类内初始值,所以使用默认构造器之后得到的值是18;而name没有类内初始值,就使用默认值,而string的默认值就是空字符串。所以打印的结果是:

hello, my name is 
I am 18 years old

注意一旦定义了其它的构造函数,就没有合成的默认构造函数了。所以下面的代码会报错:

class People {
    string name;
    int age = 18;
public:
    People(string s):name(s){}
public:
    void hello() const {
        cout << "hello, my name is " + this->name << endl;
        cout << "I am " + to_string(this->age) + " years old" << endl;
    }
    void setName(string);
};

int main()
{
    People p;   // 报错
    p.hello();

    return 0;
}

这个时候如果还需要使用默认构造函数,就需要显式定义,格式如下:

People() = default;

需要注意另外一个定义的构造函数:

People(string s):name(s){}

冒号和函数体之间的被称为构造函数初始值列表,它负责为新创建的对象的一个或多个数据成员赋值。当某个数据成员被构造函数初始化列表忽略时,它将以合成默认构造函数相同的方式隐式初始化。在下面的示例中:

int main()
{
    People p("Jack");
    p.hello();

    return 0;
}

执行结果如下:

hello, my name is Jack
I am 18 years old

注意构造函数也可以放到类外,跟普通的成员函数差不多,不过构造函数没有返回值。

有时候构造函数初始值列表是必须的,比如类的成员是const或者引用的话,因为它们必须在定义的时候初始化,所以就只能使用构造函数初始值列表,而不是在之后通过构造函数体来初始化。

构造函数初始值列表的顺序不重要,真正数据成员初始化的顺序依赖于类中定义的顺序,而不是构造函数初始值列表中的顺序。

构造函数也可以有默认实参:

class People {
    string name;
    int age = 18;
    friend void printName(People &p);
public:
    // People() = default; // 报错
    People(string s = "Jack"):name(s){}
public:
    void hello() const {
        cout << "hello, my name is " + this->name << endl;
        cout << "I am " + to_string(this->age) + " years old" << endl;
    }
    void setName(string);
    void setName(string) const;
};

int main()
{
    People p;
    printName(p);

    return 0;
}

注意这里使用了具有默认实参的构造函数之后默认构造函数就不能用了,因为两者做的事情是一致的,编译器无法区分:

Classes.cc:57:12: error: call of overloaded 'People()' is ambiguous
     People p;

当然并不是说默认实参构造函数和默认构造函数本身有冲突,只是这个例子中冲突了,下面是一个不冲突的版本:

class People {
    string name;
    int age;
public:
    People() = default;
    People(int a, string s = " "):age(a),name(s){}

实际上如果一个构造函数为所有参数都提供了默认实参,则它实际上也就定义了默认构造函数。

还有一种委托构造函数,就是用一个构造函数作为构造函数初始值列表去初始化另外的构造函数:

class People {
    string name;
    int age;
public:
    People(string s, int a):name(s),age(a){}
    People():People("Jack", 18){}

其它特性

对象的拷贝:当初始化变量,以及以值的方式传递或返回一个对象时。

对象的赋值:使用赋值运算符(=或者+=等复合型)。

类中可以包含友元。友元指的是不属于类,但是想要访问其内部Private成员的对象,这个对象可以是函数,也可以是其它类或者其它类的成员。友元使用关键字friend声明。示例:

void printName(People &p) {
    cout << p.name << endl; // 报错
}

int main()
{
    People p("Jack");
    printName(p);

    return 0;
}

因为还没有给printName()函数声明为People的友元,所以无法访问name。需要将printName声明到类中:

class People;
void printName(People &p);

class People {
    string name;
    int age = 18;
    friend void printName(People &p);
public:
    People() = default;
    People(string s):name(s){}
public:
    void hello() const {
        cout << "hello, my name is " + this->name << endl;
        cout << "I am " + to_string(this->age) + " years old" << endl;
    }
    void setName(string);
};

需要注意前面的两行代码,在有些编译器中实际上是可以省略的,主要是第二行,第一行只是因为第二行的存在才需要加上,叫作类的前向声明。而加上第二行是因为printName本身还没有声明就直接在类中使用了,最好在类之前先声明下这个友元函数。

成员函数也可以重载,比如下面两个是可以一起存在的:

void setName(string);
void setName(string) const;

如果有一组重载函数要作为类的友元,则这一组重载函数都需要分别声明,而不是只声明一个。

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

class People {
    string name;
    int age = 18;
    friend void printName(People &p);
public:
    People(string s):name(s){}
public:
    void hello() const {
        cout << "hello, my name is " + this->name << endl;
        cout << "I am " + to_string(this->age) + " years old" << endl;
    }
    void setName(string);
    void setName(string) const;
};

int main()
{
    People p = string("Jack"); // String转People
    printName(p);

    return 0;
}

如果不想要这种隐式的转换,则需要给构造函数增加explicit关键字:

public:
    explicit People(string s):name(s){}

这个时候上面的代码就会报错:

Classes.cc:56:16: error: conversion from 'std::__cxx11::string' {aka 'std::__cxx11::basic_string<char>'} to non-scalar type 'People' requested
     People p = string("Jack");

但是还是可以强制进行转换:

People p = static_cast<People>(string("Jack"));

聚合类:

  • 所有成员都是public。

  • 没有定义任何构造函数。

  • 没有类内初始值。

  • 没有基类,也没有virtual函数。

数据成员都是字面值类型的聚合类是字面值常量类。如果类不是聚合类,但是满足下列要求,也是字面值常量类:

  • 数据成员都是字面值类型。

  • 类必须至少含有一个constexpr构造函数。

  • 如果一个数据成员还有类内初始值,则内置类型成员的初始值是一条常量表达式;或者如果成员属于某种类类型,则初始值必须使用成员自己的constexpr构造函数。

  • 类必须使用析构函数的默认定义,该成员负责销毁类的对象。

虽然构造函数不能是const的,但是可以是constexpr函数。通过前置关键字constexpr来声明constexpr构造函数。constexpr构造函数可以声明成=default。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; }
    constexpr bool hardware() { return hw || io; }
    constexpr bool app() { return 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;    // hardware errors other than IO errors
    bool io;    // IO errors
    bool other; // other errors
};

聚合类,字面值常量类的作用目前还不是很清楚,它相比于普通的类有什么优势也不确定。

类可以存在静态成员:

class People {
    string name;
public:
    People(string s):name(s) {
        count++;
    }
    static int count;
};
int People::count = 0;

int main()
{
    People p1(string("Jack"));
    People p2(string("Jimmy"));
    cout << People::count << endl;

    return 0;
}

类的静态成员实体存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。使用作用域运算符直接访问静态成员。当在类的外部定义静态成员时,不能重复static关键字,该关键字只出现在类内部。一般来说我们不能在类内初始化静态成员,必须在类的外部定义和初始化每个静态成员。

静态成员可用于的某些场景,普通成员不行:

class Bar {
public:
    //...
private:
    static Bar mem1;    // 正确,静态成员可以是不完全类型。
    Bar *mem2;          // 正确,指针成员可以是不完全类型。
    Bar mem3;          // 错误,数据成员必须是完全类型。
};

不完全类型指的是类已经声明但是没有完全定义。上面声明memX的时候Bar就是不完全类型。

静态成员可以作为构造函数的默认实参,而普通成员不能。

以上是关于[C++]类基础的主要内容,如果未能解决你的问题,请参考以下文章

这些 C++ 代码片段有啥作用?

有趣的 C++ 代码片段,有啥解释吗? [复制]

以下代码片段 C++ 的说明

C++ 代码片段执行

此 Canon SDK C++ 代码片段的等效 C# 代码是啥?

C++ 代码片段(积累)