麻烦的C语言

须知:本文的Cpp编译器为gcc version 8.1.0 (x86_64-posix-seh-rev0, Built by MinGW-W64 project)

复习与拓展

类由两个东西组成:

  1. 成员变量
  2. 成员函数

成员函数在哪里?

在你心里qwq

有下面两种情况

类的定义中

如上面的代码

类的外面

如下面的代码

Code

#include <iostream>
using namespace std;
class CRectangle{
    public:
        int w,h;
        int Area();    //一定要占位
        int Preimeter(){
            return 2*(w+h);
        }
        void Init(int w_,int h_){
            w = w_;h=h_;
        }
};    //必须有分号

int CRectangle::Area(){
    return this->w*this->h;
}
struct Node{
    int nxt,var;
};

int main(){
    CRectangle* r = new CRectangle{10,3};
    cout << r->Area() << endl;

}

务必注意,类外的成员函数需要在类的里面先占位,且保证参数完全相同

成员的访问范围

概念

  • private:私有,只能在类的成员函数中访问
  • public:公有,可以在任何地方访问
  • protected:保护成员,之后再说

示例代码

class className{
    private:
        //私有属性和函数
    public:
        //共有属性和函数
    protected:
        //保护属性和函数
    private:
        //一些东西
};

使用上面的方法,三个关键字可以任意交换位置,任意形式组成,但是只能使用关键字:(换行)变量这样子的形式

当某个成员前面没有关键字,被称之为缺省情况,会被认为是私有成员。比如

class Man{
    int Age;    //private
    char szName[20];    //private

    public:
        void setName(char* name){
            strcpy(this->szName,szName);
        }
};

注意点

在类的成员函数内部,能够访问:

  • 当前对象的全部属性、函数
  • 同类其他对象的全部属性、函数

在类的外面,只能访问public成员,示例代码如下

Code

class CEmployee{
    private:
        char szName[30];

    public:
        int salary;
        // void setNmae(char* name);
        void setNmae(char* name);
        void getName(char* name);

        int averageSalary(CEmployee& e1, CEmployee& e2);
};

void CEmployee::setNmae(char* name){
    strcpy(szName,name);
}

void CEmployee::getName(char* name){
    strcpy(name,szName);
}

int CEmployee::averageSalary(CEmployee& e1,CEmployee& e2){
    return (e1.salary+e2.salary)/2;
}


int main(){
    CEmployee e,e1;
    e.setNmae("Tom");
    e.salary = 5000;
    e1.setNmae("Bob");
    e1.salary = 100;
    e.salary = e.averageSalary(e,e1);
    cout << e.salary << endl;
    
    cout << e.szName << endl; //报错
}

成员函数的性质

  • 成员函数可以重载
  • 成员函数可以缺省参数
  • 同时成员函数前也可以加inline关键字

注意!使用缺省函数时要避免函数有重载的二义性,代码如下

class Location{
    private:
        int x,y;
    public:
        void init(int x=0, int y=0){
            this->x = x, this->y = y;
        };
        void valueX(int val=0){x = val;}
        int valueX(){return x};
};

这段代码中的的valueX(int val=0)valueX()就发生了这样子的错误,就会导致编译器报错,编译器不知道该选择哪个函数。

构造函数(Constructor)

概念

相信你非常熟悉这个知识点。

构造函数支持函数重载和参数缺省,而且当然你也可以把构造函数放到类的外面。

如果不声明构造函数,则自动添加默认构造函数

默认构造函数有两种:

  1. 无参构造
  2. 有参构造:这里的有参构造需要满足所有的变量都被按顺序赋值了一遍

Code

class Location {
  private:
    int x, y;

  public:
    Location() {
        cout << "有个原点生成了" << endl;
        x = 0, y = 0;
    }
    Location(int a, int b);
    int valueX() { return x; }
};

Location::Location(int a, int b=0) {
    cout << "有新的坐标生成:" << a << "\t" << b << endl;
    x = a, y = b;
}

int main() {
    Location A = Location(1);
    Location B;
    return 0;
}

一旦自己写了构造函数,记得重载一下没有无参数的情况!!

构造函数在数组中的使用

看下面的代码

Code

class CSample {
    int x;

  public:
    CSample() { cout << "Constructor 1 Called" << endl; };
    CSample(int n) {
        x = n;
        cout << "Constructor 2 Called" << endl;
    }
};

class CSample2 {
    int x, y;

  public:
    CSample2() { cout << "Constructor 1 Called" << endl; };
    CSample2(int a1, int a2=0) {
        x = a1, y = a2;
        cout << "Constructor 2 Called" << endl;
    }
};

int main() {
    CSample array1[2]; //都用无参构造
    cout << "-----\t-------" << endl;
    CSample array2[3] = {3, 4, 5};  //调用第二构造函数
    cout << "-----\t-------" << endl;
    CSample2 array3[3] = {{1,2},{1,2},{1,2}};   //调用第二构造函数
    cout << "-----\t-------" << endl;
    CSample2 array4[3] = {{1,2}};   //仅有第一个用第二个构造函数,剩下两给没有赋值的用无参构造
    cout << "-----\t-------" << endl;
    CSample* array5 = new CSample[2];   //都是无参
    cout << "-----\t-------" << endl;
    CSample2* array6 = new CSample2[4]{{1,2},{1,2},{1}}; //前三个是构造函数2,最后一个调用无参构造

    return 0;
}

列表的话,把内容放置到后面的括号里面就好

进阶一下

Code

class Test{
    public:
        int x,y;
        Test(int n,int m=999){x=n,y=m;} //(1)
        Test(){x=0,y=0;}            //(2)
};

int main() {
    Test array1[3] = {1,Test(1,2)};
    // 三个元素分别用(1)(1)(2)初始化
    Test array2[3] = {1,{1,2}};
    // 与上面的等价
    Test array3[3] = {1,Test(1,2),Test{9}};
    // 三个元素分别用(1)(1)(1)初始化

    Test* pArray1[4] = {new Test(3),new Test{1,2}, new Test};
    // 前三个元素分别用(1)(1)(2)初始化,最后一个元素为NULL

    if(pArray1[3]==NULL){
        cout << "指针不会自动的初始化" << endl;
    }
    cout << pArray1[0]->y << endl;

    return 0;
}

复制构造函数

  • 只有一个参数,是对同类对象的引用
  • 形如T::T(T&)T::T(const T&),推荐使用后面的这种
  • 如果没有定义复制构造函数,那么编译器也会自动生成
注意:

无参构造函数不一定存在

但复制构造函数一定存在

简单的代码,缺省的情况

class Complex{
    private:
        double real,imag;
};

int main() {
    Complex c1;     //调用缺省的无参构造函数
    Complex c2(c1); //调用缺省的复制构造函数

    return 0;
}

自行实现的情况

class Complex{
    private:
        double real,imag;
    public:
        Complex(){};
        Complex(const Complex& c){
            real = c.real;
            imag = c.imag;
            cout << "Copy constructor called" << endl;
        }
};

int main() {
    Complex c1;     //调用缺省的无参构造函数
    Complex c2(c1); //调用缺省的复制构造函数

    return 0;
}

注意!不允许有形如T::T(T)的构造函数,里面必须是引用

如果你这里写的是指针,那它就不会覆盖掉编译器的复制构造函数,因为你们的参数类型不同,因此直接用重载来算

使用类型:

  1. 当用一个对象去初始化同类的另一个对象

    有两种写法

    CMyClass a1=a2;
    CMyClass a3(a2);

    注意,等号只有初始化的时候才会调用复制函数,其余情况不调用

    Code

    class CMyClass {
    public:
     int n;
     CMyClass() {}
     CMyClass(int n) { this->n = n; };
     CMyClass(CMyClass &a) {
         n = 2 * a.n;
         cout << "Copy Constructor Called" << endl;
     };
    };
    
    int main() {
     CMyClass a1(10);
     puts("-----------------");
     CMyClass a2 = a1;
     cout << a2.n << endl;
     puts("-----------------");
     CMyClass a3(a2);
     cout << a3.n << endl;
     puts("-----------------");
     CMyClass a4(1);
     a4 = a3;
     cout << a4.n << endl;
     puts("-----------------");
     return 0;
    }

  2. 如果有一个函数的参数是类A的对象,那么调用该函数时,类A的复制构造函数将被调用

    看下面的代码

    class A {
      public:
        A(){cout << "constructor called" << endl; }
        A(A &a) { cout << "Copy constructor called" << endl; }
    };
    
    void func(A a) {}
    
    int main() {
        A a1;
        func(a1);
        return 0;
    }

    会输出

    constructor called
    Copy constructor called

    所以没事传参的时候,多传指针或者引用可以加速代码

    注意点:有些时候说形参是实参的拷贝不一定正确!因为参数中如果包含一个类的对象,这个类的复制构造函数不实现复制操作的话,形参就不是实参的拷贝了
  3. vsc++: 如果函数的返回值是类A的对象时,则函数返回时,也会调用复制函数

    g++: 无

类型转换构造函数

看代码

Code

class Complex {
  public:
    double real, imag;
    Complex(int i) {    //类型转换构造函数
        cout << "IntConstructor called" << endl;
        real = i, imag = 0;
    }
    Complex(double r, double i) { real = r, imag = i; }
};

int main() {
    Complex c1(7, 8);
    Complex c2 = 12;
    c1 = 9;     // 等号右边的9会被自动转换成为临时Complex对象
    cout << c1.real << "," << c1.imag << endl;
    return 0;
}

析构函数(destructor)

概念

名字与类名相同,在前面加~,没有参数和返回值,一个类最多只能有一个析构函数。

析构函数在对象消亡时被自动调用,可以用于在对象消亡前做善后工作,如始放分配的空间等等。

如果定义类时没有析构函数,则编译器生成缺省析构函数,缺省析构函数什么也不做。

class StringM {
    private:
        char* p;
    public:
        StringM(){
            p = new char[10];
        }
        ~StringM();
};

StringM::~StringM(){
    delete []p;
}

int main() {
}

至于说什么时候对象消亡呢?后面会说

析构函数和数组

class Ctest{
    public:
        ~Ctest(){
            cout << "Destructor called" << endl;
        }
};

int main(){
    Ctest array1[2];
    cout << "END" << endl;
    return 0;
}

此时会输出

END
Destructor called
Destructor called

当然也有下面的情况

class Ctest{
    public:
        ~Ctest(){
            cout << "Destructor called" << endl;
        }
};

int main(){
    Ctest* parray1 = new Ctest[9];
    delete []parray1;
    cout << "END" << endl;
    return 0;
}

此时就会输出

Destructor called
Destructor called
Destructor called
Destructor called
Destructor called
Destructor called
Destructor called
Destructor called
Destructor called
END

什么时候会调用析构函数呢?

  1. 代码结束
  2. delete释放空间时
  3. 函数返回时:

    1. 函数局部变量全部消亡
    2. 参数和返回值(请注意,如果返回值在后面的代码中被使用,则不被销毁!!)

Code

class Ctest {
  public:
    int x = 5;
    ~Ctest() { cout << "Ctest Destructor called" << endl; }
};
class Ctest1 {
  public:
    ~Ctest1() { cout << "Ctest1 Destructor called" << endl; }
};

Ctest obj;

Ctest func1(Ctest a) {
    Ctest1 b;
    Ctest1 d;
    return a;
}

Ctest func2(Ctest &a) {
    Ctest1 b;
    return a;
}

Ctest &func3(Ctest &a) {
    Ctest1 b;
    return a;
}

Ctest *func3(Ctest *a) {
    Ctest1 b;
    Ctest1 d;
    return a;
}

int main() {
    obj = func1(obj);
    //函数内的变量先消亡触发两次析构函数,返回值消亡触发一次,参数消亡触发一次
    cout << "--------------------------" << endl;
    obj = func2(obj);
    //数内的变量先消亡触发一次析构函数,返回值消亡触发一次,参数由于是引用不调用实际的析构函数
    cout << "--------------------------" << endl;
    obj = func3(obj);
    //数内的变量先消亡触发一次析构函数,返回值和参数由于是引用不调用实际的析构函数
    cout << "--------------------------" << endl;
    Ctest *pobj = func3(&obj);
    //数内的变量先消亡触发两次次析构函数,返回值和参数由于是指针不调用实际的析构函数
    cout << "--------------------------" << endl;
    cout << obj.x << endl;
    return 0; //本身obj消亡触发一次
}

[collapse]

  1. 通过类型转换函数构建的临时变量会在赋值前生成和赋值后销毁

    [collapse title="Code" status="false"]

    #include <cstring>
    #include <iostream>
    using namespace std;
    
    class Demo {
        int id;
    
      public:
        Demo(int i) {
            id = i;
            cout << "id=" << id << "   constructed" << endl;
        }
        ~Demo() { cout << "id=" << id << "   deconstructed" << endl; }
    };
    
    Demo d1(1);
    
    void Func(){
        static Demo d2(2);
        Demo d3(3);
        cout << "Func" << endl;
    }
    int main() {
        Demo d4(4);
        d4 = 6;
        cout << "main" << endl;
        Demo d5(5);
        Func();
        cout << "main ends" << endl;
        return 0;
    }
    
    

例子

例一:

Code

#include <cstring>
#include <iostream>
using namespace std;

class Demo {
    int id;

  public:
    Demo(int i) {
        id = i;
        cout << "id=" << id << "   constructed" << endl;
    }
    ~Demo() { cout << "id=" << id << "   deconstructed" << endl; }
};

Demo d1(1);

void Func(){
    cout << "Func" << endl;
    static Demo d2(2);
    Demo d3(3);
    cout << "Func ends" << endl;
}
int main() {
    Demo d4(4);
    d4 = 6;
    cout << "main" << endl;
    {
        Demo d5(5);
    }
    Func();
    cout << "main ends" << endl;
    return 0;
}

输出结果是:

id=1   constructed
main
id=4   constructed
id=6   constructed
id=6   deconstructed
id=5   constructed
id=5   deconstructed
-------------------------
Func
id=2   constructed
id=3   constructed
Func ends
id=1   deconstructed
--------------------------
main ends
id=3   deconstructed
id=6   deconstructed
id=2   deconstructed
id=1   deconstructed

注意点:

  1. 析构顺序:先初始化的,后析构;先局部,后全局
  2. 函数的返回值如果在后文被调用,则不会触发析构函数(注意,不同的编译器效果不一致

    vsc++一定会触发析构函数,而g++不一定会触发

例二:

class A{
    public:
        ~A(){
            cout << "destructor" << endl;
        }
};

int main(){
    A* p = new A[2];
    A* p2 = new A;
    A a;
    delete []p;
}

只析构了三次!!

只要是用new关键字声明的,只要不被delete,不管程序是否结束,都会在内存中一直保留

回顾

  1. 一个类有四种特别的函数:

    1. 普通构造函数
    2. 复制构造函数
    3. 类型转换构造函数
    4. 析构函数
  2. 值得注意的是,一份代码在不同编译器下的表现完全不同,为了解决这种情况,推荐使用指针来避免编译器自己抽搐
Last modification:March 15th, 2020 at 10:42 am
Compared with money, your comment could inspire me more.
相较于钱财,你的留言更能激发我创作的灵感