【C++】初识类和对象

发布于:2023-01-04 ⋅ 阅读:(477) ⋅ 点赞:(0)

        在C语言中,定义结构体时,结构体里面只能定义变量,不能定义函数。所以在C++中引入类的概念,类可以在其中定义函数和变量。


一、类的定义

        比如这里定义一个日期类:

class Date
{
    //...
};  //一定要注意这里的分号

        使用关键字class,来定义一个类,其实会发现这里跟struct定义结构体非常相似,当然其中的很多细节是不同的。

1.访问限定符

        在类中有三个访问限定符:private(私有),public(公有),protected(保护)。其实单看名字都能大概猜出来其中的意思。

        public(公有)类里外都可以访问其变量或者函数,private(私有)只能在类里面访问。protected(保护)有点特殊后面讲。

PS:如果不写访问限定符,struct默认是public,class默认是private。

class Date
{
private:

    //...

public:

    //...

protected:

    //...

};  

2.类的定义方式

        类有两种定义方式:

        ①声明和定义分离,头文件声明放在.h文件中,定义放在.cpp文件中

        

class QueueNode
{
public:
	   QueueNode* Next;
       int val;
};

class Queue
{
public:
    void Init();
    void Push();
    void Pop();
private:
	Queue *Head;
    Queue *tail;
};

void Queue::Init()
{
    //...
}

void Queue::Push()
{
    //...
}

void Queue::Pop()
{
    //...
}

         这里的符号 ::,叫做域访问限定符,Queue:: 就表示去这个域搜索,在类外定义函数会常用。

        ②声明和定义都放在类中,不过要注意,这种写法会导致编译器可能把其函数当成内联函数处理(符合内联函数的要求的话)

class Queue
{
public:
   inline void Init();
};

PS:如果在类里面加上内联inline,会出现链接错误。想用内联直接定义就行。

二、类的实例化

class Person
{

public:
    void Print();

private:
    int age;
    char name[20];
    int sex;
};

Person A;
Person* B;

        会发现这里使用自定义的变量Person定义了一个变量A,这个A就是实例化后对象。实例化后才会消耗内存空间。

        用法跟C语言的struct相似。

        实例化后的对象就可以使用类里面的函数和变量,但是其必须用public来限定。如果用private则无法调用。

A.Print();
B->Print();

        其对象的使用方法也跟struct类似,普通的函数和变量使用 . (点)来访问。指针变量可以用  ->(箭头)  来访问。 

PS:一般是不允许访问类内部的变量的,如果要访问则需要使用接口的方式,这就叫做封装。

class person
{
public:
   
    void Print()
    {
        cout<<" 姓名: "<<_name<<" 年龄: "<<_age<<" 性别: "<<_sex<<endl;
    }

private:
    
    int _age;
    char _name[20];
    int _sex;

};

person A;

A.Print();

        这里就定义了一个Print()函数来访问类里面的私有变量。 

三、类的大小

        类计算大小跟结构体相同。但是类只单独存成员变量的大小,而其中的函数则单独存储在了一张表上。

        实例化的每个类对象成员变量是独立空间,但是调用的函数都是同一个。

class Person
{
    //...
};

Person A;
Person B;

//成员变量独立空间
A._val=1;  
B._val=2;

//函数共用
A.print(); 
B.print();

存储的三种方式:

1.假设把变量和函数都存在类里面。

        这种存储方法浪费空间,因为每个对象都存了相同的函数,一般都不会使用这种方法。

2.把函数存到一张专门来存储函数的表里面,只存了一次,要用的时候就去找。

        C++多态中使用了这种方法,其表叫做虚表

3.只存储成员变量,把函数存到了公共的函数地址列表,编译链接的时候,就在里面查找。

class Person
{

	//...

    void print()
    {

        cout<<_a<<endl;
    }

    void func()
    {
        
    }
    

    int _a;

};

A* ptr=nullptr;

//这里正常运行,因为函数不是存在类里面
//这里没有对对象进行解引用,是直接去公共地址查找的函数
ptr->func();


//这个会报错,里面调用了成员变量 _a,调用的时候会使用指向对象的一个指针来调用
ptr->printf(); 

//这个指针为空指针,解引用发生错误
this->_a

PS:

class A1
{

	void f2();
    
};

class A2
{
    
};

        这里A1和空类A2,其大小都是1字节,占位表示对象存在。

四、this指针

        

class Date
{
public:

	void init(int year,int month,int day)
	{
		_year=year;
		_month=month;
		_day=day;
	}

    void Print()
    {
        cout<<_year<<" "<<_month<<" "<<_day<<endl;
    }

private:

	int _year;
	int _month;
	int _day;
}

Date A;
Date B;

A.init(1996,10,11);
B.init(1998,20,11);

        这里定义两个对象A和B,并调用函数初始化。这里问题是,函数体内并没有对象的区分,编译器是如何知道,init()函数被调用的时候,设置的是A对象,或者是B对象呢。

        C++引入了this指针,来解决这个问题,而这个指针指向对象本身,不是其不过是被隐藏起来了,自动传入,实际上写出来也不会出错。

class Date
{
	void init(Date *const this,int year,int month,int day)
	{
		this->_year=year;
		this->_month=month;
		this->_day=day;
	}
    
    void Print(Date *const this)
    {
        cout<<this->_year<<" "<<this->_month<<" "<<this->_day<<endl;
    }
    
private:
	int _year;
	int _month;
	int _day;
}

Date* D1;
D1.init(&D1,2002,10,4);

PS:this指针是存储在栈区的,因为其是作为一个参数传入函数内部的。

PS:this指针是一个指针常量,其指向的内容可以被修改,但是指针不能被修改。

五、类中的默认函数

        类中有6个默认的成员函数,如果写一个空类,其中一定是空的吗?其实不然,编译器会帮我们生成6和默认的成员函数。

1.构造函数

         构造函数是用来初始化成员变量的,函数名和类名相同,无返回值也不用写 void,是个特殊的成员函数,支持函数重载,实例化对象自动调用,只在对象的生命周期中调用一次。

class Date
{
public:

    Date()
    {
    	_year=1;
        _month=1;
        _day=1;
    }

    //构造函数也支持函数重载,根据传入的参数,自动调用不同的构造函数
	Date(int year,int month,int day)
    {
        _year=year;
        _month=month;
        _day=day;
    }

    //全缺省的构造函数 
    Date(int year=1,int month=1,int day=1)
    {
        _year=year;
        _month=month;
        _day=day;
	}

private:
    int _year;
    int _month;
    int _day;
    
};

Date D1;

Date D2(2002,10,7);

PS:但是要注意,如果给了全缺省的构造函数,那么就不用写无参数的构造函数,不然就会有歧义,编译器不知道该调用哪个构造函数。

PS:如果没有定义构造函数,编译器自动生成一个无参的构造函数,如果自己已经定义了一个构造函数,编译器则不会生成默认构造函数。

     

          这里测试构造函数,发现默认构造函数并没有初始化成员变量。

          这是因为构造函数主要来初始化自定义类型的,因为有的类里面也会使用其他类定义的对象,这个时候让其他类的去调用自己的默认构造函数。

          因为如果在类本身的构造函数里面初始化别的自定义类型,其本身是非常不方便且困难的。

class Time
{

public:
    
    Time()
    {
        _hour=0;
        _minutr=0;
        _second=0;
    }

private:
    int _hour;
    int _minute;
    int _second;
    
};


class Date
{

private:
    int _year;
    int _month;
    int _day;

    Time _t; //这里就去调用自己的构造函数
};


Date D;

C++类型分类:

内置类型:int\char\float\指针\...

自定义类型:struct\class

 默认构造函数:a.内置类型成员不处理。b.自定义类型去调用它的默认构造函数

PS:不管什么类型的指针,指针都是内置类型。

//C++11加了一个补丁
class Date
{
    
private:
    
    //这里不是赋值,给缺省值。
    int _year = 1;
    int _month = 1;
    int __day = 1;
}    

        一般的类都不会让编译器默认生成构造函数,都会自己写。显示写一个全缺省,非常好用。        

         特殊情况下才会默认生成。

class Stack
{
    //...
};

//这里MyQueue就可以使用默认构造函数
class MyQueue
{

  int _size = 0;	//这里不会处理

  Stack popst;	    //这里去调用自己的构造函数
  Stack pushst;     //这里去调用自己的构造函数
};

2.析构函数

        对象生命周期结束后,自动调用。效果跟构造函数相反,但是不释放对象。析构函数名是在类名前面加一个~,无参数无返回值,一个类只有一个析构函数,没有显示定义,编译器自动生成一个默认的析构函数。

        默认的析构函数,内置类型不处理,自定义类型去调用它自己的析构函数。

class Date
{
    ~Date()
    {
       //... 
    }
    
}

class Test
{

public:

    ~Test()
    {
        free(_a);
        _size=0;
        _capactiy=0;    
    }

private:

    int *_a;
    int _size;
    int _capacity;

};

Date D1;
Date D2;

        定义了两个对象,要注意析构时的顺序,因为其是在栈上定义的,栈的特性先进后出,所以,先定义的后析构,所以D2会先被析构,D1再被析构。


3.拷贝构造函数

        构造函数的重载,用一个对象来初始另一个对象,参数只有一个必须是本身类型对象的引用,没显示定义,默认生成一个拷贝构造函数。

PS:拷贝构造函数的作用也是初始化,所以对象使用了拷贝构造,就不会再去调用构造函数了。

class Date
{
public:

	Date(Date& d)
	{
		_year=d._year;
		_month=d._month;
		_day=d._day;
	}

private:
    
    int _year;
    int _month;
    int _day;
};

Date d1;

Date d2(d2); //使用d1初始d2

        

class Date
{
public:

	Date(Date d)
	{
	    //...
	}

    //...
};

PS:这里如果使用传值传参,会发生无限递归的情况。因为传值传参是变量的拷贝,会发生一次拷贝构造,然后去调用拷贝构造,结果在调用的时候还是传值,所以又去调用拷贝构造,所以会发生死递归。

        如果没显示定义一个构造函数,编译器会生成一默认的构造函数,但是要注意,默认的构造函数,a.内置类型进行值拷贝,b.自定义类型去调用它自己的拷贝构造函数。                                 

        默认的拷贝构造发生的数据拷贝存在一个问题,那就是单纯进行数据的拷贝,叫做值拷贝也叫做浅拷贝。这样做会导致一些错误。

class Test
{
public:
    
    //构造函数
    Test(const char* str="hello")
    {
        _a=(char*)malloc(strlen(str)+1); //+1是为\0留的位置
        strcpy(_a,str);
    }
    
   
    //析构函数
    ~Test()
    {
        free(_a);
    }
    
private:
    char* _a;
};

Test T1;
Test T2(T1); //拷贝构造

        这里使用浅拷贝,直接把 T1._a 的值传给了 T2._a,那么就会发生错误。这里两个指针指向了 同一段空间,如果修改了T1._a里面的值,会导致T2._a里面的值也会被修改

        其次,发生析构的时候,T1._a的指针被释放后,会在T2._a被析构时,被再次释放一次,一个指针被释放两次,编译器就会报错

        所以如果类里面存在指针类型的变量,一定要为其显示定义拷贝构造函数,并且为指针类型指定一段新的空间,这种方法也叫做深拷贝

class Test
{
public:
    
    //构造函数
    Test(const char* str="hello")
    {
        _a=(char*)malloc(strlen(str)+1);

        _capacity=strlen(str);
        strcpy(_a,str);
    }
    

    //拷贝构造 
    Test(const Test& str)
    {
        char* _a = (char* )malloc(str._capacity + 1); //+1是为\0留的位置

        _capacity=str._capacity;
        strcpy(_a,str._a);
           
    }

    //析构函数
    ~Test()
    {
        free(_a);
        _capacity=0;
    }

    
private:
    
    char* _a;
    size_t _capacity; //数组的容量大小
};

        一般这里定义拷贝构造函数的时候要加上const来修饰参数。

class Test
{
public:

    //拷贝构造 
    Test(const Test& str)
    {
         //...    
    }

    Test(Test& str)
    {
         //...    
    }

private:
    char* _a;
};


const Test T1;

Test T2;

Test T3(T1);

Test T4(T2);

        首先,使用const修饰参数,这里已经算是拷贝构造函数的重载了,构造T3时就去调用const修饰的拷贝构造,构造T4时就调用普通的拷贝构造函数。

        但是这里没有必要的,只需要定义一个const修饰的拷贝构造函数就可以,都可以使用。但是注意单定义一个普通的构造函数的不行的。

class Test
{
public:

    Test(Test& str)
    {
         //...    
    }

private:
    //...
};

const Test T1;
Test T2(T1); //这里是错误的。

        因为T2进行拷贝构造,调用拷贝构造时,这里参数是一个普通的,T1会发生权限的放大,但是权限不能放大,所以这里会发生错误。

class Test
{
public:

    Test(const Test& str)
    {
         //...    
    }

private:
    //...
};

Test T1;
const Test T2(T1); 

        使用const修饰的拷贝构造函数,两种都可以拷贝。因为,T1作为参数的传入时,强制转换为const Test,T1发生了权限的缩小,权限可以被缩小。

        并且定义这种拷贝构造函数可以使用另一种拷贝构造的方法。

Test T="hello wordl"; //构造 + 拷贝构造

        要注意这种写法,不是赋值,而是构造加拷贝构造,其中发生了隐式类型的转换

        首先使用字符串要构造一个出一个临时对象,然后把临时对象拷贝构造给对象T。

        为什么使用const修饰了拷贝构造函数的参数才能使用呢,因为临时变量具有常性,使用普通的构造函数会发生权限的放大,会报错。

拓展:explicit关键字

         禁止发生隐式类型转换。

        

class Date
{
public:
    explicit Date(int year) //这里加上explicit关键字下面就不会发生隐式类型转换
        :_year(year)
    {
        
    }    

     Date(int year)
    	:_year(year)
    {
        
    }

private:

    int _year;
};


Date d1(2022)    //这里是直接调用构造函数

Date d2=2022   //这里是隐式类型转换,调用构造函数 + 拷贝构造函数
const Date& d3=2022;

//string的拷贝构造函数
string(const string& str)
{
    //...
}

void func(const string& str)
{
    //...
}
  

string s1("hello");

string s2="hello" ; //这里也是隐式类型的转换

func(s1);  

func("hello");  //这里也是隐式类型的转换

         发现支持隐式类型转换的话,这里的写法不用特地的去生成对象了,比较方便。

拓展:匿名对象

        如果只想调用一次类里面的函数,那么就可以不生成对象,直接使用匿名对象。

class Solution
{
    Sum_Solution();  
};

//匿名对象,这里生命周期只有这一行。
Solution();
	
Solution().Sum_Solution();

        类名加上括号,就是一个匿名对象,使用完成就被释放。

题目:以下代码运行了几次构造函数和拷贝构造函数?

class W
{
    W()
    {
        cout<<W()<<endl;
    }

    W(const W& w)
    {
        cout<<W(const W& w)<<endl;
    }

    ~W()
    {
        cout<<~W()<<endl;
    }

};

void f1(W w)
{
    //...
}

void f2(const W& w)
{
    //...
}


int main()
{
    W w1;
    f1(w1);     //传值传参 一次构造 + 一次拷贝构造
    f2(w2);     //传引用 不发生构造和拷贝构造
    
    f1(W());    //匿名对象传参 本来是构造 + 拷贝构造,编译器优化 只发生一次构造
    return 0;
}

PS:连续的一个表达式中,连续构造一般都会优化。

W f3()
{
	W ret;      //定义对象 发生一次构造
	return ret; //传值返回 发生一次构造 + 一次拷贝构造
}

int main()
{

    f3();    //一次构造 + 一次拷贝构造

    W w1 = f3(); //本来 一次构造 + 两次拷贝构造 ---> 编译器优化:一次构造 + 一次拷贝构造
    
}

        这里就要记清楚了,w1被实例化的时候,调用拷贝构造初始,自己就不会再调用构造函数了。

        再加上f3()里面的次数,实际上这里该有一次构造和两次拷贝构造的调用,但是编译器发生了优化,变成了一次构造和一次拷贝构造。

        这里是直接把ret的数据直接给了w1,没有再借助临时变量交换。但是ret不是函数里面的临时变量吗?出了作用域被销毁,如何能让w1接收到数据呢。

        其实这里在函数的栈帧还没有结束的时候,就直接让w1充当这个返回的临时变量,让其在放回之前就接收到了数据。 

        

        

W w2; //一次构造

w2 = f3(); 

        这里不在一个步骤内就不会优化,所以这里还是 一次构造 + 一次拷贝构造 + 一次赋值。

W f(W u)       //这里是传值传参 传入参数时会发生一次拷贝构造
{
    W v(u);    //一次构造
    W w = v;   //一次拷贝构造
    return w;  //一次拷贝构造 + 一次拷贝构造
}

int mian()
{
    W x; //一次构造
    W y=f(f(x));  
}

        所以这里是一次构造 + 七次拷贝构造。

4.运算符的重载

        C++为了增强代码的可读性,引入了运算符的重载,运算符重载是具有特殊函数名的函数,也具有返回值类型。

        重载方法为使用关键字:operator + 需要重载的运算符符号

        

//类外重载 == 符号
bool operator==(const Date& x1,const Date& x2)
{
	return x1._year==x2._year
        && x1._month=x2._month
        && x1._day=x2._day;
}


d1 == d2;  //编译器会转换为 operator==(d1,d2);

        运算符的重载不仅可以在类外定义,还可以在类里面定义。不过要注意的是,在内部定义,只有一个参数,另一个参数限定使用默认的this指针。

class Date
{
public:
    
    bool operator==(const Date& x) //里面隐藏了一个this指针
	{ 
		return _year==x._year      
        	&& _month=x._month
       	 	&& _day=x._day;
	}

private:
    //...
};


//调用的时候, 转换为 d1.operator==(d2);
d1==d2

       

          在重载"<<"、">>" ,流提取,流插入时,定义在类内部,会出现一个问题。

class Date
{
public:
    
   void operator<<(ostream& out) 
    {
        out<<"-"<<_year<<"-"_month<<"-"<<_day<<endl;
    }

private:
    //...
};

//写在类里面只能下面这样调用!对象只能是左操作数。

d1<<cout;
d1.operator<<(cout);

        会发现跟平常使用cout的时候操作看起来很别扭,因为平常是cout在前面要打印的变量在后面。所以为了改变操作数的方向,这里要在类外重载运算符。

        

//所以为了改变操作数的方向,就要写在类外面。
class Date
{
    //友元函数 能访问类的私有对象
    friend void operator<<(ostream& out,const Date& d);
};

void operator<<(ostream& out,const Date& d)
{
    out<<"-"<<d._year<<"-"d._month<<"-"<<d._day<<endl;
	
}


cout<<d1;
//实际上调用
operator<<(cout,d1);

PS:这里为了能在类外面访问类的私有变量,这里引入了一个友元函数的概念。

PS:ostream是个类,cout就是这个类定义的全局对象。cin,则是istream这个类定义的全局对象。

cout<<d1<<d2<<d3<<endl;

        然后在C++的库里面,cout可以支持多个对象。其本质就是调用多个函数,一个函数的返回值是另一个函数的参数,其调用的方向的从左到右一次调用。

        这里连续打印变量,就是让重载函数返回cout来接受下一个要打印的变量,所以为了能连续打印这里运算符重载的函数要加上返回值。

//流提取重载 注意这里是返回的引用
ostream& operator<<(ostream& out,const Date& d)
{
    out<<"-"<<d._year<<"-"d._month<<"-"<<d._day<<endl;
	return out;
}

cout<<d1<<d2;

//流插入重载 
istream& operator>>(istream& in,const Date& d)
{
    in>>d._year>>d._month>>d._day;
    assert(d.CheckDate());//检查日期是否合法。
	return in;
}

//这里因为是内置类型,所以可以直接输入。
cin>>d1>>d2;

        重载流插入时,如果有自定义类型的话,会再去调用自定义类型定义的流插入重载,而内置类型可以直接输入。

PS:注意运算符重载的时候不能改变其本身的含义,比如重载+运算符,结果得到的是相减后的结果(虽然可以这样重载)。

PS:部分运算符是不能重载的:.* (这里的点加星!,单个的*是可以重载的)、 ::sizeof?:.

        

5.赋值运算的重载(=)

        在类中如果不显示定义,编译器会默认生成一个赋值重载,所以赋值的重载只能写在类里面,不能写在类外边!!

        默认生成的赋值运算符重载,跟默认生成的拷贝构造函数有一个共同的点,就是进行的是浅拷贝,所以会出现同样的错误。

        

class Date
{
public:
    Date& operator=(const Date& d)
    {
        if(this != &d)
        {    
	    	//...
        }
    
        return *this;
    }

private:
    //...
};


d1 = d2 = d3;

        C++库里面的赋值,可以连续赋值,表达式从最右边开始赋值,说明其是有返回值的。

        而且可以自己赋值自己,所以这里要判断一下(自己赋值自己无意义,所以不用做什么)。

PS:这里的 *this 是就是对象本身。

        

int i=0;
++i;
i++;

        ++这个运算符,在变量前面和后面的意义有些不一样,一个加过后返回,一个加之前返回。所以这个符号进行函数重载的时候要进行区别。

        

//重载区分
Date& operator++() //前置
{

    *this+=1;     //!这里要注意,重载了+=才能注意写
   	return *this;

}

++d1;

Date operator++(int) //后置	这个int仅仅用来做区分的
{
    Date tmp = *this;
    *this+=1;
    return tmp;
}


d1++;

        其实也简单在括号里面写个int就是后置++,没写就是前置++。int只是作为一个区分,并没有实质上的意义。(虽然还是别扭)

6.取地址运算符的重载

Date* operator&()
{
	return this;
}

const Date* operator&()const
{
    return this;
}

        自己不写,编译器默认生成。一般什么情况都可以处理。

拓展:const成员函数

        const修饰类中的成员函数,叫做const成员函数。其实际上修饰的是this指针,表明在该成员函数中不能对类的任何成员进行修改。

        

class Date
{
public:
    Date(int year,int month,int day)
    {
        //...
    }

private:
    //...
};



const Date d1(2022,7,25);

        这里定义一个常量对象d1,但是这里会出错,调用不了构造函数。

        是this指针的问题,一般默认生成的this指针的类型是,Date* const this;表示this指针本身不能被改变。

        但是这里传入参数&d1(取地址),类型是 const Date* ,传给this指针会发生权限的放大,但是权限不能被放大,所以要给用const修饰this指针,缩小指针权限。

        但是this指针是隐藏起来的该怎么修饰呢?

        

class Date
{
public:
    Date(int year,int month,int day)
    {
        //...
    }

    Date(int year,int month,int day) const //这样写
    {
        //...
    }

private:
    //...
};

//this指针的类型就变成了
const Date* const this;

        C++规定,在成员函数后面加上const用来修饰this指针(我估计没地方放了),并且构成函数重载。(省事就把两个函数都写上吧,哪个要用就用哪个)

六、初始化列表

        上面写构造函数的时候,在函数体里面为变量赋值,其并不能称之为真正的初始化,因为初始化只能进行一次,而赋值可以进行多次。

        所以C++真正初始化要在初始化列表里面初始化。(实际上两种方法都可以用,怎么方便怎么来)

        初始化列表以:冒号开始,用,逗号隔开每个变量,在括号()里面写上要初始化的值。

class Date
{
publci:

    //这种写法严格来说不算是初始化
    Date(int year,int month,int day)
    {
        _year=year;
        _month=month;
        _day=day;
    }

        
    Date(int year,int month,int day)
        :_year(year)                      //这里就是初始化列表 变量只能出现一次
         ,_month(month)
         ,_day(day)   
    {
        
    }

private:
    int _year;
    int _month;
    int _day;

};

有些类型的变量必须使用初始化列表初始化:

①引用成员变量。②const成员变量。③没有默认构造函数的自定义类型。

class Time
{
public:                    
                          //这里不设置默认构造函数会出错
    Time(int hour = 0)    //设置缺省值,变成默认构造函数。
    {
        _hour=hour;
    }

private:
    int  _hour;
};

class Date
{
public:

    Date(int year,int hour,int& x)
        :_t(hour)   
        ,_N(10)
        ,_ref(x)	
    {
        _year=year;
    }

private:

    //这里是变量的声明
    int _year = 0; //c++11 缺省值  这里其实也是在初始化列表初始化的
    Time _t;
    const int _N;
    int& _ref;
};

int main()
{
    int y=0;
    Date day(2022,7,y);
    
    return 0;
}

        为什么没有默认构造函数会报错?

        这是因为初始化列表是变量定义的地方(private下面的只是变量的声明),也就是说你写不写初始化列表它都要初始化,只不过是随机值。

        如果没有默认构造函数,那么变量就不会被定义。

        const变量定义的时候,也只能在定义的时候初始化一次,所以这里初始化的话只能使用初始化列表。

        同理引用也是在定义的时候指定,所以也只能用初始化列表。

有的场景还是需要在函数内部初始化的。

class A
{
public:

    A(int N) 
    	:_a((int *)malloc(sizof(int)*N)) //
        ,_N(n);
    {
        if(_a==NULL)
        {
            perror("malloc fail\n");
        }
        
       	meset(_a,0,sizeof(int)*N); 
    }

private:
    int* _a;
    int _b;
};
class A
{
public:

	A(int a)
    	:_a1(a)
        ,_a2(_a1)
    {

    }

private:

  int _a2;
  int _a1;
};

A(1);

//这里 _a1 = 1, _a2 = 随机值

        然后还要注意变量初始的顺序,初始化的顺序是根据变量的声明来的,而不是在初始化列表里面的顺序

        所以这里是先初始化的 _a2,但是 _a1还没初始化是随机值,所以_a2被初始化成随机值,_a1最后被初始化为1。

七、static成员

       用static修饰的成员变量,称之为静态成员变量。用static修饰的成员函数,称之为静态成员函数。静态的成员变量一定要在类外进行初始化
        

class A
{
public:

    //这里就可以统计生成了多少个对象
    A()
    {
        ++_scount;
    }

    A(const A& t)
    {
        ++_scount;
    }
    
    //如果定义的是私有的变量 要封装一个静态成员函数做接口,来获取变量的数据
    static int GetCount()
    {
    	return _scount;    
    }
    
private:
    
    static int _scount; //声明
}

int A::_scount = 0;    //变量的初始化

A d1;
A d2;


d1._scount;
d2._scount;

A::_scount;
    
A::Getcount();    
A().GetCount();

        静态成员变量,是所有类对象共享的,不属于具体的对象,数据存放在静态区

        访问静态成员变量不需要对象直接使用类域可以直接访问。

PS:静态成员函数没有this指针,只能访问静态成员变量,不能访问任何非静态成员。但是非静态成员函数能访问静态成员。

PS:初始化列表不能初始静态成员,也不能给缺省值(这也是在初始化列表初始化)。所以只能在类外面初始化。

定义一个只能在栈上创建对象的类:

class StackOnly
{
public:

    static StackOnly CreateObj()
    {
        StackOnly so;
        return so;
    }
    
  private: 
    //设置构造函数为私有,那么就无法定义对象,只能用CreateObj()生成对象
    StackOnly()
    {
        //...
    }

private:
  	int _x=0;
    int _y=0;
};


StackOnly so2 = StackOnly::CreateObj();

        这里只能用函数创建对象,那么怎么没有对象怎么调用这个函数呢?这里就把此函数创建成了一个静态成员函数,这样就可以使用对应的类域直接调用了。

八、内部类

        把一个类定义到另一个类的里面,此类就叫内部类。

class A
{
private:

    int _h;
    static int k;

public:

    class B
    {
    public:

        void foo(const A& a)
        {
            cout<<k<<endl; //静态成员变量可以直接访问。
            
     		cout<<a._h<<endl;       
        }

    private:
        int _b;
    };

};

//这里如果计算A的大小,sizeof(A)的大小是8,跟B没关系,除非里面存了一个对象 B _b; 那么其大小是12

int A::k=0;


A::B b_test; //实例化B类的对象

        这里B类就是A的内部类,访问B就必须受A类域的限制。B天生是A的友元,但是注意A不是B的友元,A不能访问B的私有变量。

        

九、动态内存管理

        C语言中使用 malloc/calloc/realloc和free,来管理内存。而在C++使用new和delete来管理内存,不过要注意这两个不是函数,而是操作符

        

int *p1 = new int; 
int *p2 = new int[5]; //开五个int的数组
int *p3 = new int(5); //申请一个int对象,初始化为5


//c++11支持new[] 用{}初始化
int *p4 = new int[5]{1,2,3};  //后面没显示初始化的,都被初始化为了0

delete p1;
delete[] p2;

delete p3;
delete[] p4;

        使用非常方便,但是要是注意释放单个数据就使用delete,释放数组就使用delete[ ],不然会出现一些意想不到的错误。          

        对于内置类型,跟C语言的内存管理,没有本质上的区别,只是用法不同。主要的是针对自定义类型。

class A
{
public:
    A()
    {
        //...
    }

};
//1.堆上申请空间 2.调用构造函数初始化(需要有默认构造函数)
A *p2 = new A;
A *p3 = new A(0);

A *p4 = new A[2]{1,2};         //调用构造函数
A *p5 = new A[2]{A(1),A(2)};   //调用拷贝构造函数

delete p2;     //1.调用析构函数清理对象中的资源     2.释放空间

        new/delete生成失败,抛异常,不需要检查。

        在汇编能看到 其中 有一个 call operator new 的全局变量

 

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
    
while ((p = malloc(size)) == 0)
    
	if (_callnewh(size) == 0)
	{
		// report no memory
		// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
		static const std::bad_alloc nomem;
		_RAISE(nomem);
	}

	return (p);
}

        去看operator new的源码可以发现,其本质还是在使用malloc来开辟空间。而如果申请失败会在这里抛出异常。

        new失败大多数情况都是申请空间太大,或者本身内存空间不足,内存占用过多。

new/delete可以被重载:

//定义一个全局的
void* operator new(size_t size,const char* fileName,const char* funcName,size_t lineNo)
{
    void *p = ::operator new(size);

     //同时打印出在哪个文件,哪个函数,多少行,申请的大小   
    cout<<fileName<<endl<<funcName<<endl<<lineNo<<endl<<p<<endl<<size<<endl;
    return p;
}

void operator delete(void* p,const char* fileName,const char* funcName,size_t lineNo)
{
    cout<<fileName<<endl<<funcName<<endl<<lineNo<<endl<<p<<endl<<size<<endl;
    
    ::operator delete(p);
}

//调用
int* p = new(_FILE_,_FUNCTION_,_LINE_) int;
operator delete(p,_FILE_,_FUNCTION_,_LINE_);


//简化写法,直接用new/delete替换
#ifdef  _DEBUG

#define new new(_FILE_,_FUNCTION_,_LINE_)

//这个宏不用加上  直接可以使用delete,如果加上这个宏 要用 delete(p)
//#define delete(p) operator delete(p,_FILE_,_FUNCTION_,_LINE_)

#endif

int* a = new int;
delete a;


重载专属的operator new:

struct LsitNode
{
  
    int _val;
    ListNode* _next;
    //内存池
   	static	allocator<ListNode> _alloc;
    
    void * operator new(size_t n)
    {
       //allocator 是一个类  C++库里面自带的内存池
          
       void *obj = _alloc.allocate(1);
        return obj;
        
    }
    
    void operator delete(void *prt)
    {
        _alloc.deallocate(ptr);
    }
    
    struct ListNode(int val)
                    :_val(val)
                    ,_next(nullptr)
                    {}
};

allocator<ListNode> LsitNode::_alloc;

//频繁申请ListNode
ListNode* node1 = new ListNode(1);
ListNode* node2 = new ListNode(2);
ListNode* node3 = new ListNode(3);

        以后这里结构体对象申请内存时候,不去走默认的malloc,而是走自己定制的内存池。

定位new:

         在已有的一个空间调用构造函数初始化一个对象。

A* p1 =(A*)malloc(sizeof(A));

//定位new;
new(p1)A(10);