【C++】Essential c++ 第四章学习笔记

Essential c++ 第四章

  这一章主要告诉你,一个类中的基本东西都包括什么

1.类的组成

  通常一个类的基本组成元素包括成员变量和成员函数。按照权限进行分类的话,可以分为private和public两种。public的成员,意味着可以通过任何方式访问这些成员;private意味着,只有在类中才能访问这些成员,比如:

class myClass
{
public:
    int _b;
    void print()
    {
        cout<<_a<<endl;
    }
private:
    int _a;
}

int main()
{
    myClass cls;
    cout<<cls._b;
    
    //cout<<cls._a; //错误的写法
    cls.print();
    return 0;
}

  在上面创建的这个类中,_b是public的,所以myClass的实例cls可以通过类访问符进行直接访问;而_a是private的,不能通过实例cls进行直接访问,因为private的对象只能由类的成员进行访问,比如其成员函数print可以访问。

  值得注意的一点是,如果定义类成员的时候,前面不写public和private,编译器会默认这些成员是private的。

2. 类的构造

  构造函数和析构函数是一类特殊的成员函数。构造函数是类进行定义的时候进行调用的,用来初始化类中的变量;析构函数在类使用完毕后进行调用,用于清空构造这个类时所申请的全部内存空间。
  构造函数和析构函数都是没有返回值的函数,并且与类是同名的。

  如:

class myclass
{
public:
//构造函数
    myClass(int a)
    {
        _a=a;
    }
//析构函数,前面有一个~
    ~myClass()
    {
        
    }
private:
  int _a;
};

  如果你没有定义构造函数和析构函数,编译器有默认的构造函数和析构函数,他们没有任何输入参数。

2.1 构造函数

  构造函数主要是用来给类赋予初值的,有函数体内赋值和初始化列表赋值两种,同时也可以像非类成员函数那样具有默认值。

  • 函数体内赋值

class myclass
{
public:
    myClass(int a)
    {
        _a=a;
    }
private:
  int _a;
};
  • 初始化列表

   初始化列表跟在构造函数参数列表之后,用:进行定义


class myclass
{
public:
    myClass(int a,int b)
    :_a(a),_b(b){}
  
private:
  int _a;
  int _b;
};
  • 带有默认值的构造函数

class myclass
{
public:
    myClass(int a=1,int b=1)
    :_a(a),_b(b){}
  
private:
  int _a;
  int _b;
};

2.2 析构函数

  析构函数前面具有一个~,也没有返回值,下面是最简单的析构函数

~myClass()
{
    
}

2.3 拷贝构造

(1)实例的复制机制

  我们在进行类创建的时候,往往会需要以原有的实例创建新的实例对象比如

class myClass
{
public:
  myClass(int a):_a(a){}
private:
  int _a;
};
myClass a(1);
myClass b = a; //进行复制

  那么myClass = a;这句话,到底发生了什么情况呢?在第一章的内容中,我们知道下面这两种对变量的定义方法是等效的。

int a = 1;

int a(1);

  对类的拷贝来说,也是一样的。
myClass b=a和myClass b(a)是等价的。myClass b(a)这种写法与构造函数非常相似,但是我们定义的构造函数并不接受myClass这种类型的参数输入,那么对象b到底是怎么被构造的呢?

  原来,c++中,有种默认的拷贝构造函数,能够接受同类对象。因此,实际上b(a)这种写法,调用的不是构造函数,而是默认拷贝构造函数,默认拷贝构造函数会在下一个小节进行讲述,下面我们只是简单的来验证一下默认拷贝构造函数的存在。

#include<iostream>
using namespace std;
class myClass
{
public:
  myClass()
  {
      cout<<"函数被构造!"<<endl;
  }
  ~myClass()
  {
      cout<<"函数被析构!"<<endl;
  }
private:
 
};

int main()
{
    myClass a;
    myClass b(a);
    
    return 0;
}

  从输出结果来看,这组程序构造函数被调用了一次,析构函数被调用了两次,正是因为默认拷贝构造函数的存在 b(a)这种写法没有使用构造函数被创建,因此第二句话也就不会调用构造函数。

(2)默认拷贝构造函数

  知道了默认拷贝构造函数的存在之后,我们再来说明一下默认拷贝构造函数会做什么。

  默认拷贝构造函数实际上是进行的一一复制操作,也b(a)之后,会把a中的所有数据变量完全复制到b中相应的数据变量中去。如果一个数据变量是一个指针类型的话,拷贝构造之后,会使得两个变量指向同一个地址。如果你的类中,涉及到地址的话,复制的时候就要格外小心了,以防内存访问错误问题,比如下面这样:

myClass
{
public:
    myClass()
    {
      int *p = new int[3];
    }
     ~myClass()
    {
        delete []p;
    }
private:
    int *p;
};

int main()
{
    myClass a;
    myClass b(a);
}

  这种情况下使用默认的拷贝构造函数就是一种非常危险的情况,因为new定义的变量是单独的,必须通过delete才能够使电脑释放这部分内存,而经过默认的拷贝构造后,a和b的变量p指向了同一块内存,第一个析构函数调用时,会清空这个内存。第二个析构函数调用时,又会来清空这块内存,但是这块内存以及刚刚被清除了,于是就会发生内存访问错误,为了避免这种情况的发生,必须要使用自定义的拷贝构造函数

(3)自定义拷贝构造函数

  自定义拷贝构造函数,就是定义一个输入类型为自身的构造函数。

myClass
{
public:
    myClass()
    {
      int *p = new int[3];
    }
    myClass(myClass m)
    {
        int *p = new int[3];
        for(int i=0;i<3;i++)
        {
            this.p[i]=m.p[i];
        }
    }
     ~myClass()
    {
        delete []p;
    }
private:
    int *p;
};

int main()
{
    myClass a;
    myClass b(a);
}

  上面举了一个可能不是非常恰当的例子,但是通过自定义的拷贝构造函数,能够使得新建一个保存p内存的空间,就不会发送内存访问冲突的问题。

3. 类成员的管理

3.1 通过const和mutable进行类修改权限管理

(1)cosnt

  当你定义一个成员函数时,如果这个成员函数并没有改变对象内的任何一个数据时,就可以把它设置为const,const意味着这个类不能对对象作出修改
比如:

#include<iostream>
using namespace std;
class myClass
{
public:
    myClass(int a) :_a(a) {}
    void print() cosnt  
    {
        cout << _a << endl;
    }
    void a_plus()
    {
        _a++;
    }
private:
    int _a;
};

int main()
{
    myClass cls(1);
    cls.print();
    cls.a_plus();
}

  比如像上面的程序一样,print没有修改myClass任何成员的值,因此可以定义为const,而a_plus函数修改了成员_a的数值,因此不能被定义为const

  这里需要注意一点,如果成员函数返回值为引用或者指针,意味着接受返回值的人,可以通过地址修改类内部的成员,这种函数依旧不能够被设置为const的


#include<iostream>
using namespace std;
class myClass
{
public:
    myClass(int a) :_a(a) {}

    myClass& ptr() 
    {
        return *this;
    }

    int _a;
};

int main()
{
    myClass cls(1);
    myClass& b = cls.ptr();
    b._a = 2;
    cout << cls._a;

}


  比如此处的ptr函数,返回值是个reference,通过接受reference的对象能够修改原来对象的值,因此也算是间接修改类内部的成员,定义const是非法的。

(2)mutable

  mutable是针对变量来说的,还使用上面那个类来说明,但是也是举了一个可能不是很恰当的例子

#include<iostream>
using namespace std;
class myClass
{
public:
    myClass(int a) :_a(a) {}
    void print() const
    {
        cout << _a << endl;
    }
    void a_plus()
    {
        _a++;
    }
private:
    int _a;
};
void f(const myClass& cls)
{
    cls.a_plus();
    cls.print();
}

  这里定义一个函数,我们可以函数输入类型是const myClass,这个时候调用cls.a_plus会报错,因为这个类并不允许函数修改成员,如果我们想要使用这两个函数,必须这两个函数都是cosnt的,才能使用,可是a_plus函数修改了成员变量,不能定义为const,这个时候就可以使用mutable来修饰变量 _a,说明在const的函数中,修改_a是合法的。

#include<iostream>
using namespace std;
class myClass
{
public:
    myClass(int a) :_a(a) {}
    void print() const
    {
        cout << _a << endl;
    }
    void a_plus() cosnt
    {
        _a++;
    }
private:
   mutable int _a;
};
void f(const myClass& cls)
{
    cls.a_plus();
    cls.print();
}

3.2 通过静态进行内存管理

(1)静态变量的特性

  对于static的变量来说,一般包含有两层含义:

  (1)隐藏性,这就意味着,在这个cpp文件里面定义的变量,其他cpp文件不能调用,这是跟全局变量很大的一个区别。假设有一个全局变量

int i;,

那么如果再另外一个cpp文件里面有

extern int i;

那么另外一个cpp文件就能够找到这个全局变量,并且使用它。而如果这个变量被static修饰了,那么其他文件就找不到这个变量了

(2)永久性。永久性意味着static的变量具有全局变量的特性,在main函数运行之前被定义好,只能发生一次初始化,并且全局有效。

(2)静态的成员变量

  如果一个类的变量是静态的,那就意味着这个类的所有对象都能够使用这个变量,并且这个变量值是固定的。

  • 但是我们要注意一个问题就是,如果这个变量能够脱离具体对象存在,必须现在程序里面给他一个存放的位置,让他能够生存。所以,程序前面会有一句话 int A::i 这样之后,静态变量i才能够在类A里面存在。或者从另外一个角度来考虑,类里面全都是声明,而不是定义,就相当于写了一句 extern int i;只能说明程序里面有这个变量i,但是他具体定义在什么地方,并不知道,我们就需要告诉编译器静态变量的定义位置

  • 我们要注意的第二个问题是,静态变量初始化一般在类外面进行,如果一定在类内部进行的话,一定不能使用初始化列表的方法,因为这个变量只能初始化一次,使用初始化列表逻辑上就是每个实例被创建的时候,都会被初始化因此构造函数如果要给静态变量赋予初值的话,要写 A(){ i = 0}; 而不能写A:i(0){}。二者意义不同,前者是赋值,后者是初始化

  • 既然静态变量可以脱离实例存在,那么使用A::i这种方式也是可以直接访问变量的。不过要注意,i必须的public的,如果是private的,是不能访问的。

  • 关于int A::i;这句话,再做一点说明,这个定义前面是不能加static的,因为从c语言角度考虑,static意味着i不能被其他cpp文件访问了,但是类中的变量应该要被其他cpp文件访问
    所以,这么定义的话,编译会无法通过,违背了类的本来的设计目的

(3) 静态的成员函数
  • 静态对象的函数和静态对象的变量有相似的性质,都可以脱离实例生存,因此可以通过A::f()的方式进行访问。但是注意,通过这个方式访问静态函数,静态函数使用的所有变量也必须的静态的,如果变量不能脱离实例存在,那么函数是不可能调用这些变量的。因此静态函数内部,只能出现静态的变量

  • 静态对象的函数是没有隐藏参数this的,因为this依赖于具体的实例,静态函数能够脱离实例,为了不产生冲突,静态函数没有隐藏参数this

(4) 静态的使用情景

  类的每个实例都需要有一个固定不变的值(比如银行固定利率),而且希望改变的时候能够把所有实例的这个值全都改变,这样就可以使用静态特性

例程


#include<iostream>
using namespace std;

class A
{
public:
	static int i;

	static void f()
	{
		cout << i;

	}
	A() {  }
};

int A::i;//给静态变量创建了一个容器

int main()
{
	A a;
	a.i = 10;
	A b;
	cout << b.i << endl;
	cout << A::i;//静态变量可以脱离实例生存
	A::f();//静态函数可以直接调用,脱离实例生存
	return 0;
}

3.3 通过友元进行访问权限管理

  我们有时候可能会希望设计的类的私有变量,可以仅仅被某个函数或者某个类访问,这时候我们就用到了友元,来为这些函数或类赋予访问权限。
  对函数赋予访问权限的方法就是 friend + 函数定义
  对类赋予访问权限的方法就行 friend class +类名
  友元类中的所有成员函数,都可以访问它的私有的成员变量

#include<iostream>
using namespace std;
class myClass
{
public:
    myClass(int a) :_a(a)
    {

    }
    friend class ClassB;
    friend void f(myClass A);
private:
    int _a;
};

class ClassB
{
public:
    void f(myClass A)
    {
        cout << A._a << endl;
    }
private:

};

void f(myClass A)
{
    cout << A._a << endl;
}
int main()
{
    myClass A(1);
    ClassB b;
    b.f(A);
    f(A);
}


3.4 this指针

  this指针指代的就是类定义的实例其本身,有时候我们可能需要返回类自己,就可以使用this指针。

4.运算符重载

  运算符重载本质就是一种特殊的成员函数。有时候我们可能会希望+能够把两个类中的某些变量相加,或者*能够实现指向类的某个成员变量,这个时候,就用到了运算符的重载,在类中定义我们自己的运算符。
  运算符重载使用关键词operator来标识

4.1 运算符重载的规则

  • 不能改变运算符的变量个数,本来是二元运算符必须给两个变量,不能改变运算符的计算数
  • 不能创造新的运算符
  • 运算符的变量中,必须出现类,不能对现有的运算符进行重载
  • 不能改变运算符的优先级

4.2 双目运算符的重载

  双目运算符就比如: + = * / == !=等等。
  运算符重载可以在类内进行,也可以在类外进行,我们来看一下具体写法

(1)在类外重载运算符

  因为运算符重载必须满足操作数相同,我们对加法运算符重载,就必须提供两个参数:

class myClass
{
public:
  int _a;
private:
};

int operator+(myClass& A,myClass &B)
{
    return A._a + B._a;
}

(2)在类内进行运算符重载

在类内重载运算符的时候,只需要一个参数就够了,因为在前面有一个隐藏的参数*this

class myClass
{
public:
  myClass(int a):_a(a){}
  int _a;
  int operator+(myClass &B)
{
    return _a + B._a;
}
private:
};
(3)类内运算符重载的实质

仍然使用上面那个函数的+运算符重载

int main()
{
    myClass cls;
     cls + 33 + cls;
    
    return 0;
}

上面两句话

cls + 3;

3 + cls;

那句话可以被正常执行呢?答案是第一句话,因为类内运算符重载本质上是一种成员函数,等价于

cls.+(3);

  因为myClass类的构造函数是需要一个int类型的变量的,3可以直接送给myClass的构造函数,使得3可以被当做myClass的实例对象来使用。

(4)运算符重载的tips

  运算符重载中,尽量减少重新定义运算符,而使用以及定义过的运算符进行,这样的话,如果以后要修改函数的话,可以减少修改个数,比如逻辑运算符

class myClass
{
public:
  myClass(int a):_a(a){}
  int _a;
  bool operator==(myClass &rhs)
  {
      return _a==rhs._a;
  }
   bool operator!=(myClass &rhs)
  {
      return !(*this==rhs);
  }

};

4.3 单目运算符的重载

  单目运算符主要指++,因为++可以分为运算符在后和运算符在前两种,c++对这两种形式进行了区别

//++运算符前置版 ++a
myClass& operator++(){}
//++运算符后置版 a++
myClass operator++(int){}

4.4 提领运算符的重载

int operator *()cosnt;

4.5 函数调用符的重载(函数对象)

  函数对象的内容在前面第三章的学习笔记中以及介绍过了,本质上,函数对象就是一个函数调用符的重载。多用在STL算法中

4.6 赋值符的重载

  赋值符重载之前,一定要注意的问题就是先判断赋值对象是否等于自身。如果等于自身的话,假设使用了new生成了内存空间,要delete掉旧的,执向新内存。在这种情况下,删除旧的之后,新内存以及没有地方可以指向了,就会报错。

  比如:

Matrix& operator=(const Matrix & mat)
{
    if(this!=&mat)
    {
        this._row = mat._row;
        this._col = mat._col;
        int elem_cnt = this._row*this._col;
        delete [] _pmat;
        _pmat = new double[elem_cnt];
        for(int i=0;i<elem_cnt;i++)
        {
            _pmat[ix]=mat._pmat[ix];
        }
        
    }
    return *this;
}

4.7 iostream的重载

(1)定义在类外

一种比较常见的定义


class myClass
{
    
public:

    int length;
};

ostream& operator <<(ostream& os,myClass &rhs)
{
os << rhs.length << endl;
return os;
}

使用方法

    myClass cls;
    cout<< cls;

(2)另外一种定义在类外的方法

class myClass
{
    
public:


    int length;
};

ostream& operator <<(myClass &rhs,ostream& os,)
{
os << rhs.length << endl;
return os;
}

使用方法


    myClass cls;
    //因为里面的两个参数,第一个是类,第二个是os,
    //就相当于<<的左边那个参数和右边那个参数,所以要倒着写,看起来很奇怪
   cls << cout;

(3)定义在类内

class myClass
{
    
public:
ostream& operator <<(ostream& os,)
{
    os << length << endl;
    return os;
}

    int length;
};

使用方法


    myClass cls;
    //因为里面的两个参数,第一个是类(this),第二个是os,
    //就相当于<<的左边那个参数和右边那个参数,所以要倒着写,看起来很奇怪
   cls << cout;

5.指针—指向类成员函数的指针

  指向类成员的指针和指向普通函数的指针比较相似,下面我们来看一下指向类成员的指针如何使用

  假设有这样一个类

#include<iostream>
using namespace std;
int main()
{
    class myClass
    {
    public:
        void f()
        {
            cout<<"1"<<endl;
        }
            
    private:
    }
    
    
    return 0;
}

  我们定义一个指向类成员函数的指针,这个指针和普通函数指针的区别在于,指针一定要指定class scope,并且与类中的函数具有相同的形式。并且通过取地址符,把函数地址交给函数指针。使用的时候,必须通过类的实例来调用


//函数指针的定义
void (myClasss *p)()=0;

//函数指针的赋值
p = &myClass::f;

//使用
myClass cls;
(cls.*p)();
//等价于 cls.f();

  以上内容可以进行简化,用嵌套类型可以更加方便

#include<iostream>
using namespace std;

class myClass
{
public:
    void f()
    {
        cout << "1" << endl;
    }
    typedef void (myClass::* ptr)();

private:
};


typedef myClass::ptr ptr;
int main()
{

    myClass cls;
    ptr p = &myClass::f;
    (cls.*p)();

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值