a. 在C语言阶段我们就已经认识到了结构体,它可以帮助我们实现多种数据结构,功能强大不言而喻。在C++中为了更好的封装,以及更好的将现实生活抽象化为计算机世界,采用面向对象的思想帮助程序猿能够对用户需求进行抽象思维化,不得不需要类class的出现。
C语言中的struct只能定义变量,但C++中的struct不仅可以定义变量也可以定义函数,他和class关键字的作用是相同的,都可以定义类,当然这是C++为了兼容C语言对struct做出的改变,所以struct所定义的类的成员默认是公有的,就是无论谁都可以使用,因为在C语言中我们是可以直接获取到结构体里面的所有数据的。
而class定义的类的成员默认是私有的,类内不被限制,类外不可以直接获取到类中的变量,也就是类中的数据,这也正是面向对象语言的一大特性,封装,你只能通过类中的函数来访问数据,不可以直接访问到类里面的数据。
//class Stack
struct Stack
{
//类体,由成员函数和成员变量组成
void Init(int N = 4)
{
top = 0;
capacity = 0;// 访问限定符限制的是类外面的,类里面不会被限制
}
void Push(int x)
{
}
int* array;
int top;
int capacity;//C++把类看作一个整体,编译器搜索的时候会在整个类里面去搜索。C语言为了编译的效率只会向上搜索。
};//不要丢掉分号
int main()
{
Stack st;// 用类stack实例化出对象st
st.top = 0;//这里的语句,在class类会被限制 struct类不会被限制
}
a. 首先大家要知道,类只是一个抽象的描述,对象才是一个具体的东西,就像int,double,char等类型,他们也只是一个描述,他们所定义出来的变量才是具体的东西,所以程序中是不存在类这样的东西的,因为它不是具体化的东西,自然类是不占用内存的。
b. 类的描述信息,程序在编译的时候只要其语义,运行时是不需要他的,因为到运行阶段的时候,代码都已经转换为二进制指令了,二进制指令命令电脑去做对象与对象之间的交互,哪还有类这样的概念。当然如果要用类中的成员函数,成员函数是在代码段的,所以光对于类来说,他仅仅是方便程序猿写程序的一个抽象化描述
c. 在C++的程序中,编译链接之后的指令,宏观角度来说,其实是对象之间利用接口进行交互以及单个对象和接口之间的交互,这种宏观的概念里面哪还有类啊,只要编译器了解类的语义之后,类就没用了,所以根本没有必要分配给它内存。
a. 声明和定义分离的话,那函数就会被当作正常函数对待,在调用的地方建立函数栈帧,分配堆栈的空间。
b. 函数定义时的函数名前要加上类域名
//使用时,要指定类域
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
cout << _name << " " << _gender << " " << _age << endl;
}
内联方面的知识:
如果声明和定义都放在类里面,函数会向编译器发出内联请求,是否同意这取决于编译器,这个知识了解一下就好。
推荐加下划线的方式来声明成员变量
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
a. 访问限定符分为public private protected,现阶段我们只要会用public和private就可以。
b. public修饰的成员公开对外,类外可之间使用,访问限定符的作用域从其出现的位置到下一个访问限定符之间的区域,下一个可以以右括号}结束作用域。
class Person
{
public:
void PrintPersonInfo();
private:
char _name[20];
char _gender[3];
int _age;
};
c. 访问限定符只在程序编译时有用,等到程序运行阶段,没有任何访问限定符的区别,因为都已经转换为二进制指令了。
a. 封装就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。说白了就是将方法的实现和属性隐藏起来,只让用户和产品进行交互,隐藏实现的种种细节,方便更好的管理。
b. C++中我们实现封装其实就是通过类和访问限定符,将对象的内部实现细节隐藏起来,通过访问限定符将某些接口放开在类外部进行使用。
局部作用域:
函数内部的区域我们称之为局部作用域,其中的变量形参等,我们都称之为局部变量,因为他们都存在于函数栈帧里面,是一个局部的区域。
类域:
类域是为了对象的更好封装,将类外和类内划分边界,利用域作用限定符,选择性的将某些功能对外开放。
全局作用域:
全局就是指,在一个工程中的不同文件之间都可以使用你所定义的全局变量或全局函数,也就是在整个程序中都可以使用,额外补充一个点,加static修饰的全局变量或全局函数,他们由之前的外部链接属性改为内部链接属性。
namespace域:
命名空间域,这个域里面可以由程序猿来放一些可能与库中函数或关键字起冲突的东东,和类一样,我们也可以选择性的放开命名空间中的功能对外开放,通过利用三种使用命名空间域的方式。
a. 利用类类型创建对象的过程我们称之为类的实例化过程
b. 类是对对象进行描述的,它可以限定类有哪些成员,但定义出一个类是不占用实际的内存空间的。
c. 我们可以利用一个类来创建多个实例化的对象
class A
{
public:
void PrintA()
// 从操作系统角度来看,不可能把所有的指令都存下来,只存函数的地址,指针大小4个字节,所以大小应该是5个字节,内存对齐就是8个字节
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
cout << sizeof(A) << endl;
}
可能会有人认为这个类A的类型大小是8字节,因为函数地址占4个字节,int占4个字节,根据结构体内存对齐原则,就是8字节。
但其实不是这样的,我们在计算过程中其实只关注了成员变量,至于函数并没有关注,这是为什么呢?这就涉及到实例化对象模型的设计了,我们往下再看看吧。
a.
为了节省实例化对象所占空间,我们将每个对象的成员函数抽离出来,放在公共代码段,这样在使用函数时,每个对象只要去公共代码段里面调用就可以了,里面放着该类所有成员函数的有效地址
b.
所以在计算类类型大小的时候,我们是不关注成员函数的,因为实例化出来的对象中根本就不存储函数地址,函数地址都被放在公共代码段了,我们可以类比C语言部分的知识,C语言在算某个类型所占字节时,是不也可以算该类型创建出来的变量所占字节,由此可见我们算类类型大小,其实就是算对象的大小。
虽然这个类是空的,但是还可以创建对象不是吗?所以编译器给空类类型一个字节的空间用于标识这种特殊的类型
class A
{
};
int main()
{
cout << sizeof(A) << endl;
}
a.
我们知道一个成员函数是可以被多个对象所调用的,那成员函数怎么知道它现在作用的是哪个对象呢?万一有10个对象都调用同一个函数,函数只作用于第一个对象,这可怎么办啊?无法满足我们的需求啊。
b.
所以C++编译器给每个“非静态成员函数”增加了一个隐藏的this指针来作为函数的形参, 并且规定该参数必须在函数形参的最左边位置,这个指针中存储的就是对象的地址,在函数体中所有访问对象成员变量的操作都是通过this指针来完成的,只不过这些操作对于用户是透明的,用户不需要手动传递对象地址,编译器可以自动完成。
c.
我们不可以手动去传递this指针,这是编译器的工作,我们不能去抢,但是我们可以在函数体内部使用这个this指针。
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;//使用this指针访问到对象的成员变量_year
_month = month;
_day = day;
}
void Print()// 在函数体内部我们可以使用this指针。
{
cout << this << endl;
cout << this->_year << "-" << this->_month << "-" << _day << endl;//我们加了this,编译器就不加了,我们不加,编译器就会加
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Init(2022, 12, 15);
Date d2;
d2.Init(2022, 12, 16);
d1.Print();//d1调用,访问的就是d1的成员
d2.Print();//d2调用,访问的就是d2的成员
cout << &d1 << endl;
cout << &d2 << endl;
return 0;
}
a.
原因很简单,对象的地址,我们是不希望修改的,因为你一旦修改,原先地址所指向的空间内容我们就无法访问到了,所以C++规定this指针是不能被修改的,隐藏的形式就需要const来修饰指针本身。
b.
有关const和指针的三种写法:
const Date* p1;
Date const* p2;
上面这两种写法都是等价的,const放在星号的左边,修饰的就是 * p1和* p2,也就是指针指向的对象
Date* const p3;// 下面这种写法修饰的是p3,const放在*右边,修饰的是指针变量p3本身,
a.
前面说过成员函数会被转换为指令放在公共代码段,所以这时候可能会有人以为this指针存在代码段,其实this指针存在栈上,下面我们来分析一下原因
b.
代码在经过编译链接阶段后会变成二进制指令,我们平常所说的函数栈帧建立以及堆上malloc开辟空间等等,都是在程序的运行阶段开辟的,而不是编译或链接阶段,空间的开辟是需要机器一步一步识别指令,根据指令内容来进行空间的开辟,所以成员函数会被编译成二进制指令,机器看到调用函数的指令后,就开始开辟函数栈帧,在开辟的栈帧里面存放局部变量和形参等,所以this指针其实是存在栈区上的
c.
还有一个问题就是,我们上面说过,类成员函数是有可能被当作内联处理的,我们都知道内联函数他是一个特殊函数,他是不建立函数栈帧的,所以如果是这样的情况呢?this指针又该存在哪里啊?有了上面知识的讲解,就好解决这个问题了,由于内联函数不建立栈帧,在编译阶段就被展开在相应的位置了,所以在二进制指令里面就没有建立函数栈帧的指令,自然就没有形参的概念,所以这种情况,this指针根本不存在。
下面给出了两端代码,大家可以看一下,哪个代码正常运行,哪个代码跑起来有问题。
class A
{
public:
void Print()
{
cout << "Print()" << endl;
// 不发生空对象的成员访问,对象中不存在成员函数,成员函数都被转换成二进制指令放在代码段里面了
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
class B
{
public:
void PrintB()
{
cout << _a << endl;
// 你实例化出来的对象是空的,然而你又通过指针去访问空对象中的数据,那就是典型的空指针访问,this->_a,读取访问权限冲突
}
private:
int _a;
};
int main()
{
B* p = nullptr;
p->PrintB();
return 0;
}
我将答案写到了代码注释里面,想要解决上面的题目,其实要明白空指针的成员访问,空指针马,那就是什么都没有,它里面是空的,所以无论你是对空指针进行成员访问或是解引用,其实都会报运行错误,注意是运行错误,而不是编译错误,只有在程序跑起来时,编译器才会报运行错误,语法检查上面,编译器认为你的这两个行为是合理的。
我们上面提到过类型占一个字节的空类,空类中什么都没有吗?还是他有但是我们看不到?
其实空类中是有东西的,他有编译器默认生成的6个成员函数,如果我们不主动去写默认成员函数,编译器是会自动生成他们的。
对象的初始化≠对象的创建
下面代码如果将注释内容放开,代码就会编译失败,因为我们显示定义了构造函数,编译器就不会生成构造函数了,但是我们显示定义的构造函数无法完成实例化对象的初始化工作,所以代码就会编译失败。
a. 补充一个概念:
默认构造函数:全缺省构造函数,无参构造函数,编译器默认生成的无参数无代码的构造函数
构造函数:这个范围较大,包括默认构造函数,以及那些无法完成对象初始化工作的构造函数。
要点:
如果没有可用的构造函数,则编译器会自动调用该类的无参构造函数,但如果我们自己显示定义了构造,且构造发挥不了作用,那么编译器就不会自动生成构造函数,此时如果还没有无参构造函数的话,则编译器就会报错没有合适的默认构造可用。
C++关于默认构造函数和无参构造函数(转载自csdn博主xiaoju223的文章)
class Date
{
public:
/*
// 如果用户显式定义了构造函数,编译器将不再生成
Date(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;
};
int main()
{
Date d1;
return 0;
}
b. 编译器对于内置类型不处理,对于自定义类型会调用他的构造函数,这个构造函数既有可能是默认构造也有可能是无法完成初始化工作的构造函数。
c. 内置类型:编译器自带的类型,int/char/double/所有指针,包括类类型的指针
自定义类型:就是我们自己定义出来的类型,class/struct + Stack/Queue/Person。对于这样的类型,编译器会自动调用该类的构造函数。
d. 针对编译器不处理内置类型这样的漏洞,C++11做出了补丁,即内置类型成员变量在类中声明时可以给出默认值,但需要注意的是虽然我们肉眼可见的是定义出了一个个变量,但原则还是不能变的,类依旧是不占用内存的,给出的声明只有在实例化对象的时候,才会使用这个声明值,才会占用内存。
class Date
{
private:
// 基本类型(内置类型)
int _year = 1970;
int _month = 1;
int _day = 1;
// 自定义类型
Time _t;
};
对象的销毁≠对象中资源的清理
a. 编译器默认生成的析构函数对于内置类型并不会处理,在对象生命结束时,操作系统会自动回收内置类型的内存,但对于自定义类型,编译器默认生成的析构函数会调用该类类型的析构函数。
b. 值得注意的是,由于编译器默认生成的默认析构不会处理内置类型,这也就为它功能的缺陷埋下了隐患,例如内置类型中声明了malloc开辟在堆上的空间呢?这种情况下继续依靠编译器默认生成的析构显然无法满足资源的清理工作,这时候就需要我们手动去将申请的空间还给操作系统。例如栈类的析构函数就需要我们自己来写,他的构造函数同样也需要我们自己来写,因为编译器提供的默认构造无法满足我们的要求。
class Stack
{
public:
Stack(int capacity = 4)
{
//iostream可能间接包含C语言中的头文件
_array = (int*)malloc(sizeof(int) * capacity);//他妈的这里开辟了一个随机数大小的空间我真服了。
if (_array == nullptr)
{
perror("malloc fail");
exit(-1);
}
_top = 0;
_capacity = capacity;
}
~Stack()
{
free(_array);
_array = nullptr;
_top = _capacity = 0;
}
void Push(int x)
{
_array[_top++] = x;
}
private://如果我们想要在调试窗口看到对象的成员变量,可以敲个this指针,这样就可以看到对象的全部成员变量
int* _array;
int _top;
int _capacity;
};
a. 拷贝构造函数是构造函数的一个重载形式。
b. 拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// Date(const Date& d) // 正确写法
Date(const Date d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
c. . 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝
如果我们的类是Date类,可以很容易想到,我们不用自己写出拷贝构造,因为他的内置类型都是一些int整型,但如果是Stack类,情况就不一样了,因为他的内置类型里面有指针,此时一旦发生拷贝构造,两个对象中的指针指向的就是同一块空间,那么在在两个对象生命结束时所调用的析构函数就会讲相同指针所指向的空间释放两次,第二次释放的地址就是一个无效的地址,这块地址根本没有指向一块儿有效的空间,自然程序就会出现错误。
d. 调用拷贝构造函数的场景有哪些?
1>使用已经存在的对象去拷贝创建新对象。
int main()
{
Date d1(2022, 12, 16);
Date d3(2022, 12, 12);
// 例如我想拷贝一份d1
Date d2(d1);// 拷贝构造
}
2>函数参数类型为某个类的对象
3>函数返回值为某个类的对象。(这是规定,不要去考虑返回值较小寄存器带出这样的事情,只要返回就需要调用拷贝构造,和那个没关系)
e. 为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用尽量使用引用。
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
a. 作为类成员函数重载的时候,参数其实是要少一个的,因为左操作数所对应的参数是隐藏的this指针。如果你要写成全局的重载的话,参数和操作数个数就会相等,因为你需要将两个操作数的形参都写出来
b. .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
//bool operator==(Date* this, const Date& d2)
bool operator==(const Date& d)//这里用传值传参不好,因为值拷贝要调用拷贝构造,而且比较里面不会改变对象,所以要用const修饰
{
//成员变量是私有的,我们怎么解决呢?
//放到类里面由于this指针的存在,导致参数过多,因为这个函数只允许两个参数存在
return d._year == _year &&
d._month == _month &&
d._day == _day;
}
//上面是类域的运算符重载,下面是全局域的运算符重载。差别就是非静态成员函数有this指针
bool operator==(const Date& d1, const Date& d2)
{
return d1._year == d2._year &&
d1._month == d2._month &&
d1._day == d2._day;
}
int main()
{
cout << (d1 == d2) << endl;// d1为左操作数,默认的运算符重载的this指向的就是d1
}
c. 补充:
我们知道如果重载写成全局,那其实需要一个前提,就是成员变量需要默认为共有,因为如果是私有的话,外面是访问不到这些成员变量的,但为了封装性,我们一般又不愿意将成员变量公开,下面给出两种解决方案:
1.在类里面写一个共公有的函数,用于获取成员变量,然后在全局重载里面调用这个公有函数,以这样的方式来访问到私有的成员变量。
2.将全局重载写成友元函数,下面会讲到友元函数。
a. 我们最早使用的看起来较为高级的自动识别类型的输出和输入语句,其实利用的就是运算符重载,只不过系统给我们写好了这些重载函数,ostream和istream就是类,cin和cout分别为这两个类所实例化出来的对象,在使用<<流插入运算符或>>流提取运算符的时候,就会调用类中相应的operator>>函数和operator<<函数,所以自动识别类型的本质还是运算符重载。
#include
using namespace std
由下面C++库中的istream和ostream类中的包含的函数可以看到,确实有很多类型被包含了。
b. 我们可以用cin或cout输出内置类型,但对于自定义类型,系统是无法支持的,所以这时候就需要我们自己重载自定义类型的流插入和流提取。
需要注意的是,我们是想调用对象cout/cin的重载函数,按照我们的使用习惯cout<
//写在Date类的形式:
void operator<<(ostream& out)//两个对象,cout和d1
{
out << _year << "年" << _month << "月" << _day << "日" << endl;
}
//调用时的形式:
int main()
{
d1<<cout;// 相当于d1.operator<<(cout)
return 0;
}
//写在全局的形式
class Date
{
//友元声明(类的任意位置,不用加inline),我是你的朋友,我来偷你家桃子了,你别放狗咬我。
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
//利用友元便可以解决类外函数无法访问到类内私有成员变量的问题。
//………………………………省略一部分Date类的内容
}
// operator<<(cout,d1) cout<
inline ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;//成员是公有的才可以这么用,私有需要友元来解决
return out;
}
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
void DateTest4()
{
Date d1, d2;
cout <<d1;// 相当于 cout.operatotr<<(d1),这样的形式调用,重载必须写在全局
cin >> d1;
cout << d1 << d2 << endl;// 调用我们自己写的运算符重载 void operator<<(cout,d1)
//endl可以不用重载,因为endl是内置类型,其实就是字符'\n'
cout << d1 - d2 << endl;
}
int main()
{
DateTest4();
return 0;
}
由于.h文件会在多个源文件中展开,所以一旦.h文件中有全局函数或变量,在文件的链接阶段就会出现符号重命名的问题,因为每个源文件的符号表里面都有相同的函数名和有效地址。
解决的方案有3个:
1 将全局函数的声明和定义分离,这样就算.h文件展开到不同的源文件当中,这些源文件中只有拥有函数定义的源文件的符号表才会有真正有效的函数名和有效地址,其他源文件的符号表中即使有函数名但是没有函数的有效地址。
2 在全局函数前面加上static,这样全局函数就会由原来的外部链接属性转换为内部链接属性,此时符号表中是不会存放加static这样函数的函数名和地址的,因为这样的函数没有存放在符号表的意义,在之前的博文程序环境和预编译中我们谈到过,符号表存在的意义其实就是为了让函数能够跨文件使用,使得各个文件不再是独立的个体,联合成一个整体的程序,现在你的函数已经变为内部链接属性,不允许其他源文件使用,自然也就失去了进入符号表的意义。
话又说回来,如果函数不进入符号表,那在链接阶段自然也就不存在重命名的问题了。
3 在函数前面加上inline修饰,函数就不会走开辟函数栈帧这样的路子,而是在被调用的地方展开,值得注意的是,我们并不需要管这个函数展不展开,而是需要注意只要加上inline,函数名和地址就不进符号表的,只要不进符号表,多文件包含.h文件中的全局函数的重命名的问题就得以解决。
下面是证明过程,在C++入门那篇博文我们就提到过,如果代码过长,编译器为了防止代码膨胀的发生是会拒绝展开代码的,如果不展开代码,自然函数就要被调用,也就会开辟函数栈帧等等,有些人可能会觉得如果开辟了函数栈帧,那不就和.h文件中的全局函数在多个.c文件下引发的重命名问题没区别了嘛?
事实如下,就算inline不展开代码,函数也不会被当作全局函数处理,因为我们看到链接阶段,程序没有发生问题,自然也就说明,只要全局函数加上inline,那他就不会进入该文件的符号表内。
解决这个问题,就又回到对于类的本质的理解上面了,由于类仅仅只是一个类型,是对对象的抽象化的描述,所以描述不占用内存,类也就不占用内存。
所以更别说链接阶段了,编译阶段类都不存在。只要编译器语义检测过后,类就没有用了,自然就不会出现声明重命名的问题了。
a. 赋值运算符重载格式
1.参数类型:尽量使用引用,这样可以减少调用拷贝构造,而且赋值的对象并不会被修改,所以我们最好再加上const修饰。
2.返回值类型:返回*this的引用,这样既可以支持连续赋值,又可以减少性能的开销,因为我们用的是引用,不需要调用拷贝构造。
3.检测是否自己给自己赋值:如果是自己给自己赋值,就没有必要赋值了,所以我们加一个if语句的判断条件来控制这样的情况。
class Date
{
// …………………………省略Date类的部分内容
public :
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// …………………………省略Date类的部分内容
private:
int _year ;
int _month ;
int _day ;
};
b. 赋值重载不可以写成全局函数。
如果我们没有在类的内部写赋值重载,编译器就会默认生成一个赋值重载,此时如果还在类的外面写全局的赋值重载的话,势必就会出现冲突,所以赋值重载只能写在类里面。
也许有人可能会有疑问,那之前的流插入和流输出重载为什么就可以写到外面呢?他怎么不会和库里面的重载起冲突啊,因为我们写的和库中的其实构成了重载函数,并未冲突,因为库中的流插入或流输出重载参数只有一个,只要接收对应变量的内置类型即可,而我们重载的流插入和流提取的参数是类的实例化对象,也就是自定义类型,而不是内置类型,所以他们构成了函数重载,不会发生冲突。
c. 和拷贝构造比较相似的是,对于内置类型,赋值重载做的也是浅拷贝,一旦内置类型涉及到开辟空间时,浅拷贝就不起作用了,这个时候就需要深拷贝,对于自定义类型,赋值重载会调用该类类型的赋值重载。
与拷贝构造不同的是,赋值重载的对象是已经存在的对象,而拷贝构造的对象是创建的新对象。
d.如果类中涉及到资源管理,我们不自己去写赋值重载的话,实际上会出现两个特别严重的问题,两个对象的指针都指向不同的空间,一旦发生赋值重载,两个指针就会指向同一块空间,另一块空间就会内存泄露。并且两个指针指向的同一块儿内存空间被释放两次也会出现问题:越界访问,程序崩溃。
下面的Stack类中便涉及到了资源管理,此时就需要我们自己去写赋值重载。
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType *_array;//向操作系统申请内存资源
size_t _size;
size_t _capacity;
};
int main()
{
Stack s1;
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
Stack s2;
s2 = s1;
return 0;
}
我们知道前置++返回的结果应该是+1之后的结果,后置++应该返回的是+1之前的结果但在运算符重载的时候,这两个运算符都是++,如果不做区分很容易混淆,所以C++规定后置++重载时需要多加一个参数int,但调用该函数时不用传参数,编译器会自动传递,此时前置++和后置++便可以顺利构成重载函数。
// C++
Date& Date:: operator++()
{
_day += 1;
return *this;// 出了作用域*this对象还在,因为是main栈帧里面的,所以可以用引用返回,这样可以减少调用拷贝构造
}
// C++规定后置++重载时,需要多一个int参数,主要是为了和前置区分开来,构成函数重载
Date Date:: operator++(int)
{
Date tmp(*this);// 拷贝构造
_day += 1;
return tmp;// 返回++之前的值
}
构造函数和析构函数对于内置类型不会处理,但该调用还得调用。对于自定义类型会调用该类的构造和析构函数,值得注意的是如果内置类型有需要释放的空间,那析构函数就需要自己写,因为编译器对于内置类型不会处理。
拷贝构造和赋值重载对于内置类型会进行浅拷贝,对于自定义类型会调用该类的拷贝构造和赋值重载函数,不同的是前者是对创建的新对象进行拷贝,后者是对已经存在的对象进行拷贝,值得注意的是,如果内置类型出现我们申请的空间,那我们是需要自己写一个深拷贝的,也就是自己写一个拷贝构造或赋值重载。
class Date
{
public:
Date* operator&()
{
//return this;
return nullptr;
}
const Date* operator&()const// 这个地方的知识点可以先去看一下const成员的讲解,看完自然就可以理解,这里的函数为什么设计成这个样子。
{
//return this;
return nullptr;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};
int main()
{
Date d1;
const Date d2;
return 0;
}
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,如果你想依托取地址操作符&获得对象或者其他你想要的东西,那这个时候你需要在类里面重新去写取地址重载,或者你不想让&获得地址,而是屏蔽掉对象的地址,那你可以返回nullptr。
所以,只要你不作,你就不用写这两个重载,编译器默认生成的就够用
1.
将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数
隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
class Date
{
public:
void Print()const// 相当于const Date* const this,这样this指向的内容就是可读了,这里的const只修饰this指向的内容,其他参数不受影响
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
void DateTest5()
{
//看到const转换的错误什么的,大概率都是出现权限放大
Date d1(2022, 12, 12);
d1.Print();//可读可写 到 可读,权限缩小
const Date d2(2022, 12, 18);
d2.Print();//可读 到 可读可写,权限放大
//d2是被const修饰的,说明对象本身不可被修改相当于const Date*或Date const*这样的指针形式
//但类成员函数中的this指针又是Date* const this,至于为什么是这样子,
//我们之前提到过,对象的地址是不应该被随意修改的,所以编译器将this参数默认为上述样子,
//可以看到如果不想让权限放大,我们必须在*的前面加上const
//由于this是隐形的,所以编译器规定在函数括号后面加const来表示此对象不可被修改。
}
int main()
{
DateTest5();
return 0;
}
总结:const对象不可以调用非const成员函数,因为这会导致权限放大,非const对象可以调用const成员函数,因为这是权限的缩小。
const成员函数内部是不可以调用其他的非const成员函数的,因为const成员函数内部不允许对类的任何成员进行修改,而调用非const成员函数,是有可能在const成员函数内部修改类成员的,所以这就导致了权限的放大。
非const成员函数内部可以调用const成员函数,因为在非const成员函数内部,修改或不修改类成员都是可以的。
a.
之前我们所学的构造函数可以给新创建的对象赋初值,这点我们是知道的,但是它并不是对成员变量的初始化,而是赋值工作。
因为初始化只能初始一次,而构造函数内的赋值是可以赋值多次的。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)// 初始化列表
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
b.
每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
c.
当类中的成员变量出现const修饰、引用成员变量,或自定义类型的类中没有合适的默认构造的时候必须要在初始化列表的位置进行初始化,不可以通过构造函数对成员变量进行赋初值。
默认构造函数:全缺省构造函数,无参构造函数,编译器默认生成的无参数无代码的构造函数。
至于第三个应该也是好理解的,因为如果自定义类型没有合适的默认构造的话,那他就失去了被赋值的权利,所以他必须得在初始化的时候进行 “ 赋值 ”,否则编译器会报错没有合适的默认构造。
例如下面的类A实例化出来的 _aobj对象,如果将构造函数改为无参的形式的话,则不会报错。
d.
如果在初始化列表显示写了,就用显示写的初始化
如果没有在初始化列表显示初始化
1.内置类型,有缺省值用缺省值,没有就用随机值,缺省值其实也是在初始化列表位置使用的。
2.自定义类型,初始化列表会调用他的默认构造函数,如果没有默认构造函数就会报错。
所以尽量用初始化列表来初始化,因为无论你写不写初始化列表,所有的成员变量在进入构造函数之前都要走一遍初始化列表。
class Date
{
public:
Date(int year=2023,int month=1,int day=1)
:_year(2024)
,_month(1)
,_day(1)
{}
private:
int _year = 2022;
int _month = 12;
int _day = 23;
};
int main()
{
Date d1;
return 0;
}
e.
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
所以下面代码会先初始化_a2再初始化_a1,那么输出的_a1就应该是1,_a2就应该是随机值。
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<<endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();// 答案:程序会输出1和随机值
}
a. 构造函数不仅可以构造和初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。单参数是C++98就已经支持的,多参数是C++11才开始支持的。
b. 一旦使用explicit来修饰构造函数,将会禁止构造函数的隐式转换。
class Date
{
public:
// 1. 单参构造函数,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换---explicit去掉之后,代码可以通过编译
explicit Date(int year)// 单参的构造函数支持了隐式类型转换
:_year(year)
{}
// 2. 虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
// explicit修饰构造函数,禁止类型转换
explicit Date(int year, int month = 1, int day = 1)//全缺省也可以,我们也可以只传一个参数
: _year(year)
, _month(month)
, _day(day)
{}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//直接调用构造,下面多了一个临时变量的拷贝构造而已。 当然优化之后两个其实可以看作是一样的。
// 隐式类型的转换
Date d2 = 2022;// 用整型2022去构造一个Date类的临时对象,在用临时对象去拷贝构造d2,这里会直接优化为构造。
// 本来:构造+拷贝构造 优化:直接构造
const Date& d5 = 2022;// 引用的是2022构造的临时对象,临时对象具有常性,所以需要用const修饰
// 这里不会做优化,必须先构造再拷贝构造,因为他是引用
return 0;//程序正常运行结束
}
这里给大家提几个小细节,对于单参数的默认构造我们可以选择第一个参数不给出缺省值,也可以选择给出缺省值,这样的两种情况都是可以进行隐式类型转换的,
多参数构造在函数形式上就是多个参数没有给出缺省值,这倒是没有什么新颖的,但是在实例化对象时的使用方式就有些叫人眼前一亮了。
class Date
{
public:
// 多参数
Date(int year, int month , int day )
: _year(year)
, _month(month)
, _day(day)
{}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = { 2022,12,18 };
Date d2(2022, 12, 18);
//上下写法等价,结果一样,但是过程不一样,一个是直接构造,一个是构造+拷贝构造
const Date& d3 = { 2022,12,18 };// 引用的是临时变量,具有常性用const修饰
//C++11也是延申了C++98里面的单参数构造。
return 0;
}
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化
以上都是C++关于static的硬性规定,先记住,然后我用代码来帮助大家理解。
题目:现在要求你实现一个类来计算出程序中创建出了多少给类对象。
分析:对于题目我们首先想到的是要在构造和拷贝构造函数里面都加上一个计数器,每次调用这两个函数中的任意一个,计数器就会+1,所以我们最后只要输出这个计数器的值就可以了,但是这个时候就会存在一个问题,如果我们仅仅只是普通声明一个cnt变量,这当然是不行的,因为每次++,不会加到同一个cnt上,加的是不同对象里面的cnt成员变量,所以cnt不能在对象里面,cnt应该在静态区,这个时候就引出了我们的静态成员变量的作用了。
随之又会延申出另一个问题,静态成员变量是私有的,我们还得通过公有函数让类外面来访问到私有变量。公有函数既可以是静态也可以是非静态的。
class A
{
public:
A(int a = 0)
:_a(a)
{
++N;
}
A(const A& aa)
:_a(aa._a)
{
++N;
}
static int GetN()// 静态成员函数没有this指针,只能访问静态成员(变量或函数),静态无法访问非静态。
//所以静态成员函数其实是和静态成员变量配合起来用的。
{
return N;
}
private:
int _a;
// 类中的static成员变量属于类,并且类的每个对象是共享的
static int N;//类里面的静态会受到类域的限制
//所以现在有三种静态,全局静态,局部静态,类静态,他们生命周期都是全局的,但作用域是不同的
};
int A::N = 0; //静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
int main()
{
//**** aa1和aa2是存在栈上的,类静态N是存在静态区(全局区)的
A aa1(1);
A aa2 = 2;//会不会出现构造呢?
//编译器优化了,所以这里直接拷贝构造,如果不优化答案就应该是4.
A aa3 = aa1;
cout << aa1.GetN() << endl;
cout << A::GetN() << endl;
return 0;
}
非static成员函数可以调用类的静态成员函数,因为非静态属于某个特定的对象,而静态被该类的所有对象共享,那么通过某个具体对象来访问所有对象共享的内容当然是可以的。
但是反过来不可以,因为静态没有this指针,所以静态无法确切的指向某个具体的对象,自然就无法通过对象的地址(this指针)来获取到对象中的变量或函数方法。
非static成员函数可访问static态成员函数/成员;static成员函数不能访问非static成员函数/成员,只能访问static成员函数/变量(转载自csdn博主持续学习,不断沉淀的文章)
C++中static、const、static const修饰变量作用详解(转载自csdn博主sevenshal的文章)
提醒:友元函数的使用场景,我们在重载流插入和流提取运算符的时候提到过,这里只对友元函数的特性进行总结,如果有忘了的同学可以返回去看一下上面的内容。
友元类的所有成员函数都可以是另一个类的友元函数,并且友元类里面可以访问另一个类中的所有成员,完美实现无缝偷家。
注意:友元关系是单向的,不具有交换性。友元关系不能传递
class Time
{
friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
Time(int hour = 0, int minute = 0, int second = 0)
: _hour(hour)
, _minute(minute)
, _second(second)
{}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{}
void SetTimeOfDate(int hour, int minute, int second)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
概念:如果一个类定义在另一个类的内部,这个内部类天然就是外部类的友元类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
1.注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
2.sizeof(外部类)=外部类,和内部类没有任何关系。
3.内部类可以定义在外部类的任意位置,public、protected、private都是可以的。
B类的访问受A的类域和访问限定符的限制
class A
{
private:
int _a;
static int k;
public:
class B // 内部类B天生就是A的友元
{
int _b;
void foo(const A& a)
{
cout << k << endl;//内部类可以直接访问外部类中的static成员
cout << a._a << endl;//通过A类对象访问A类中的成员变量。
}
};
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl;
// A中没有B,算的是单纯A的大小,仅仅只是嵌套定义
A aa;
A::B bb;
return 0;
}
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
// 匿名对象 -- 生命周期是当前这一行
A();//在这一行定义完之后,下面的无论哪行都无法用这个对象,并且立马会调用析构函数
A(3);
//Solution so;
//so.Sum_Solution(10);
// 这里创建so对象就只是为了调用函数,有点白白浪费。这时候可以用到匿名对象
Solution().Sum_Solution(10);//创建匿名对象调用函数
return 0;
}
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
A f3()
{
return A(10);
}
int main()
{
// 优化场景1
A aa1 = 1; // A tmp(1) + A aa1(tmp) --> 优化 A aa1 = 1
// 优化场景2
A aa1;// 构造
f1(aa1);// 拷贝构造
f1(A(1));// 匿名构造+拷贝构造 --> 优化 构造(直接拿1去构造形参了)
f1(1);// 隐式类型转换 匿名构造+拷贝构造 --> 优化 构造
f2();// 构造 返回时候的拷贝构造也被优化了
A ret = f2();// 构造+两次拷贝构造 优化为直接构造A ret
A ret;// 拷贝构造
ret = f2();// 构造+拷贝构造+赋值重载 直接优化为赋值重载
A ret = f3();//本来是构造+两次拷贝构造 优化为直接构造对象ret
return 0;
}