4.3 .h文件中全局变量或函数引发的重命名问题如何解决?
由于.h文件会在多个源文件中展开,所以一旦.h文件中有全局函数或变量,在文件的链接阶段就会出现符号重命名的问题,因为每个源文件的符号表里面都有相同的函数名和有效地址。
解决的方案有3个:
1 将全局函数的声明和定义分离,这样就算.h文件展开到不同的源文件当中,这些源文件中只有拥有函数定义的源文件的符号表才会有真正有效的函数名和有效地址,其他源文件的符号表中即使有函数名但是没有函数的有效地址。
2 在全局函数前面加上static,这样全局函数就会由原来的外部链接属性转换为内部链接属性,此时符号表中是不会存放加static这样函数的函数名和地址的,因为这样的函数没有存放在符号表的意义,在之前的博文程序环境和预编译中我们谈到过,符号表存在的意义其实就是为了让函数能够跨文件使用,使得各个文件不再是独立的个体,联合成一个整体的程序,现在你的函数已经变为内部链接属性,不允许其他源文件使用,自然也就失去了进入符号表的意义。
话又说回来,如果函数不进入符号表,那在链接阶段自然也就不存在重命名的问题了。
3 在函数前面加上inline修饰,函数就不会走开辟函数栈帧这样的路子,而是在被调用的地方展开,值得注意的是,我们并不需要管这个函数展不展开,而是需要注意只要加上inline,函数名和地址就不进符号表的,只要不进符号表,多文件包含.h文件中的全局函数的重命名的问题就得以解决。
下面是证明过程,在C++入门那篇博文我们就提到过,如果代码过长,编译器为了防止代码膨胀的发生是会拒绝展开代码的,如果不展开代码,自然函数就要被调用,也就会开辟函数栈帧等等,有些人可能会觉得如果开辟了函数栈帧,那不就和.h文件中的全局函数在多个.c文件下引发的重命名问题没区别了嘛?
事实如下,就算inline不展开代码,函数也不会被当作全局函数处理,因为我们看到链接阶段,程序没有发生问题,自然也就说明,只要全局函数加上inline,那他就不会进入该文件的符号表内。
4.4 为什么.h文件中的类不会发生重命名问题呢?
解决这个问题,就又回到对于类的本质的理解上面了,由于类仅仅只是一个类型,是对对象的抽象化的描述,所以描述不占用内存,类也就不占用内存。
所以更别说链接阶段了,编译阶段类都不存在。只要编译器语义检测过后,类就没有用了,自然就不会出现声明重命名的问题了。
4.5 赋值重载(默认成员函数:两个已经存在的对象之间的赋值)
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; }
4.6 前置++和后置++重载
我们知道前置++返回的结果应该是+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;// 返回++之前的值 }
5.关于前面四个默认成员函数的小总结
构造函数和析构函数对于内置类型不会处理,但该调用还得调用。对于自定义类型会调用该类的构造和析构函数,值得注意的是如果内置类型有需要释放的空间,那析构函数就需要自己写,因为编译器对于内置类型不会处理。
拷贝构造和赋值重载对于内置类型会进行浅拷贝,对于自定义类型会调用该类的拷贝构造和赋值重载函数,不同的是前者是对创建的新对象进行拷贝,后者是对已经存在的对象进行拷贝,值得注意的是,如果内置类型出现我们申请的空间,那我们是需要自己写一个深拷贝的,也就是自己写一个拷贝构造或赋值重载。
6.普通对象、const对象取地址重载(这俩函数不重要,了解即可)
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。
所以,只要你不作,你就不用写这两个重载,编译器默认生成的就够用
六、const修饰的类成员函数(const修饰的是成员函数中隐含的this指针所指向的内容,const T* const this)
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和随机值 }
八、explicit关键字(对象的隐式类型转换)
a. 构造函数不仅可以构造和初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有类型转换的作用。单参数是C++98就已经支持的,多参数是C++11才开始支持的。
b. 一旦使用explicit来修饰构造函数,将会禁止构造函数的隐式转换。
1.单参数构造(C++98)
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;//程序正常运行结束 }
这里给大家提几个小细节,对于单参数的默认构造我们可以选择第一个参数不给出缺省值,也可以选择给出缺省值,这样的两种情况都是可以进行隐式类型转换的,
2.多参数构造(C++11)
多参数构造在函数形式上就是多个参数没有给出缺省值,这倒是没有什么新颖的,但是在实例化对象时的使用方式就有些叫人眼前一亮了。
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修饰的成员变量,称之为静态成员变量;用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; }
静态函数无法访问非静态成员的根本原因就是因为他没有this指针,因为对象中的函数的调用以及成员变量的访问等等,其实都是通过隐形的this指针来完成的,你现在没有this指针,当然就无法访问这些非静态成员了。
类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
非static成员函数可以调用类的静态成员函数,因为非静态属于某个特定的对象,而静态被该类的所有对象共享,那么通过某个具体对象来访问所有对象共享的内容当然是可以的。
但是反过来不可以,因为静态没有this指针,所以静态无法确切的指向某个具体的对象,自然就无法通过对象的地址(this指针)来获取到对象中的变量或函数方法。
十、const和static的用法详解
C++中static、const、static const修饰变量作用详解
十一、友元+内部类
1.友元函数
提醒:友元函数的使用场景,我们在重载流插入和流提取运算符的时候提到过,这里只对友元函数的特性进行总结,如果有忘了的同学可以返回去看一下上面的内容。
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,并且友元类里面可以访问另一个类中的所有成员,完美实现无缝偷家。
注意:友元关系是单向的,不具有交换性。友元关系不能传递
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; };
3.内部类
概念:如果一个类定义在另一个类的内部,这个内部类天然就是外部类的友元类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
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; }