在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);