本小节是构造函数与成员变量相关的笔记。
包含:
- 函数:默认构造函数、拷贝构造函数、类型转换构造函数、移动构造函数(待写)、析构函数、静态成员函数
- 重载:运算符重载(简略)、函数重载
- 函数其他:对象的构造与析构顺序、重写重载和覆盖、浅拷贝
- 变量:列表初始化、初始化顺序、成员变量的内存对齐
平时都在用,但是总是判断错误:在成员函数中可以访问同类对象的所有属性!
默认构造函数指的是无参构造函数,在有些情况下非常重要:如创建的对象数组不能全部初始化,后续的需要默认初始化;再如被组合到其他类中的类,没有默认初始化则对应的类也必须为其初始化。
因此定义类的同时尽量要定义一个默认构造函数。
class ObjectFunc { ObjectFunc() { ... } }
只要提供了一个构造函数(无参、有参、拷贝等),编译器就不会默认合成一个构造函数。若需要编译器自动合成的默认构造函数,可通过ObjectFunc()=default;
声明实现。
构造函数一般设置为公有public,否则在main函数中无法调用进行初始化。
#include <iostream> #include <vector> #include <ostream> using namespace std; class ObjectFunc { public: // ObjectFunc()=default; ObjectFunc(const ObjectFunc& a) { this->v = a.v; } private: int v; int t = 9; char d{'a'}; }; int main() { ObjectFunc a; //报错:类ObjectFunc不存在默认构造函数 return 0; }
在很多场景下需要使用一个对象来初始化另一个对象,若不定义拷贝构造函数,则编译器自动合成一个拷贝构造函数。编译器自动合成的拷贝构造函数执行浅拷贝,成员变量含有指针时,则会使两个指针指向同一片区域,一个对象的析构会释放此空间。因此,若成员变量为指针时,一定要定义拷贝构造函数。(也有利用浅拷贝的,比如智能指针中的共享指针share_ptr)
特点:只有一个参数,必须为引用类型,常常使用const对参数修饰
用法:
ObjectFunc(const ObjectFunc& a) { ...//执行拷贝赋值 }
用途(3个):
使用同类对象初始化时:=、()、{}
方式初始化都会调用
传递类对象参数为值传递方式时:void exampleFunc(ObjectFunc a)
返回值为类对象时:ObjectFunc exampleFunc1(){return ObjectFunc();}
//class ObjectFunc ObjectFunc(const ObjectFunc& a) { this->v = a.v; cout << "调用拷贝构造函数" << endl; } //main //初始化 ObjectFunc b = a; ObjectFunc c(a); ObjectFunc d{ a }; //返回值 ObjectFunc exampleFunc1() { ObjectFunc d; return d; } //值传递 void exampleFunc2(ObjectFunc a) { ; }
为什么传递参数不能为传值?
因为值传递同样需要构造一个新的类对象,这造成无限循环调用拷贝构造函数。
初始化与赋值的区分
类对象的初始化和赋值都使用等号,区别在于初始化是定义的同时给予初值,调用了拷贝构造函数;而赋值直接执行浅拷贝。
ObjectFunc a; ObjectFunc b = a;//定义并初始化 ObjectFunc c; c = a;//赋值
如何更好的避免浅拷贝?
类内定义了指针时,要考虑定义拷贝构造函数、赋值重载函数、以及析构函数。这三个函数是绑定在一起的。
临时变量
ObjectFunc(); ObjectFunc obj = ObjectFunc();
单独说一下,上述代码并不是仅仅是函数调用,同时还构造了一个临时变量。若此临时变量有其他定义的变量直接承接,相当于临时变量直接转正,不存在拷贝构造函数的调用。
在类和对象之基础中已经对右值引用以及使用做了简单的笔记。右值引用重点针对的是即将销毁的对象或者右值,如临时对象、不再使用的变量、表达式。在函数中常常将局部变量创建临时对象方返回,调用了拷贝构造函数。在此处可以使用右值引用+std::move(局部变量)就不需要调用拷贝构造函数了。
//class ObjectFunc ObjectFunc(string s) { ++i; cout << "ObjectFunc调用次数:" << i << endl; } static int i; int ObjectFunc::i = 0;//类外 //func void typeConverFunc(ObjectFunc s) { ; } ObjectFunc returnTypeConverFunc(string& s) { return s; } //main ObjectFunc g = string("asd");//初始化 typeConverFunc(string("dfgh"));//值传递 string s{ "zxcv" };//返回值 returnTypeConverFunc(s); //output ObjectFunc调用次数:1 ObjectFunc调用次数:2 ObjectFunc调用次数:3
当构造函数有一个参数时,可以被作为类型转换构造函数,通过类型转换创建一个实例。若不想让其进行类型转换,就可以在只有一个参数的构造函数前添加explicit
关键字,从而禁止调用类型转换构造函数。
explicit ObjectFunc(string s) { ++i; cout << "ObjectFunc调用次数:" << i << endl; } ObjectFunc returnTypeConverFunc(string& s) { return s;//不存在用户定义的从string到ObjectFunc的转换 }
在使用explicit
关键字时,.h文件的成员函数声明前可使用,但是类外的成员函数定义处就不能再使用了。
string类中没有定义explicit关键字,因此可以从字符串到string类的转换,但是vector容器中定义了ecplicit,因此就不能类型转换。
对象数组使用参数初始化时,参数部分调用了类型转换构造函数,而后续没有初始化的部分调用的是默认构造函数。
ObjectFunc obj[10] = {"qwe", "asd", "fgh"};
若不自行定义析构函数,编译器会自动创建一个析构函数。若类中定义的成员变量都是内置类型,那么使用默认析构函数就能在对象被销毁时自动回收内置类型成员变量的空间。但是如果存在指针,那么默认析构函数只能回收指针变量的空间,而指针所指向的空间没有被释放,存在内存泄漏。因此,拷贝构造函数、赋值运算符重载以及析构函数一般是同时定义。
析构函数要定义为public,若不自行定义析构函数,编译器会自动合成一个析构函数,可能造成内存泄漏。
~ObjectFunc() { delete pobj; }
在继承中,子类不仅要初始化自行定制的成员变量,还要初始化父类定义的成员变量(不论是否能够访问到),因此在析构时父类与子类的析构函数都要调用。为了达到这个目的,在继承使用中,析构函数一定要定义成虚函数,而后先调用子类的析构函数,再调用父类的析构函数。
待写...
C++中的类型决定了分配多少内存空间、如何解释此部分内存空间的数值、此数值能够进行运算。内置类型如整型变量能够进行加减乘除、赋值、比较大小、输入以及输出等。为了让自定义的类型也能使用运算符进行操作,因此对运算符进行重载。
这些运算符重载本质上是一次函数调用,因此也归纳到本小节的笔记中。
重载运算符的返回类型通常情况下应该与内置版本的返回类型兼容:
运算符 | 返回类型 |
---|---|
关系或者逻辑 | bool |
算术 | 类类型值 |
赋值、复合赋值 | 左侧对象引用 |
在定义的时候往往定义一套,如定义了>
理应定义其他的运算符;定义了==
理应在此基础上定义!=
;定义了+=
理应在此基础上定义=
。
运算符 | 要求 |
---|---|
. :: .* ?: | 不能被重载 |
= [] () -> | 必须定义为成员函数 |
>> << | 必须定义为全局 |
对称性运算符如算术、逻辑、关系 | 尽量定义成全局函数 |
紧贴对象的运算符如* ++ -- | 尽量定义成成员 |
综上所述,四个不能重载、四个必须定义为成员、两个必须定义为全局,其余的都是建议。
为什么对称性的尽量定义为全局呢?比如string的+
运算符定义为成员,则只能进行string+"casd"
形式,反过来却不行。但是定义为全局则string+"asd"
或者"asd"+string
都可。
逻辑运算符&&
和||
重载后,没有此运算符的短路规则,因为本质上是函数调用。
重载为全局函数(声明为友元)和成员函数。
class ObjectFunc { friend ostream& operator<<(ostream& os, const ObjectFunc& object); public: //重载为成员函数 ObjectFunc& operator+=(const ObjectFunc& a) { this->v += a.v; return *this; } private: int v; }; //重载为全局函数,要声明为友元 ostream& operator<<(ostream& os, const ObjectFunc& object) { os << object.v; return os; }
《C++ Primer 第5版》
重载运算符 | 位置 |
---|---|
输入输出 | P494 |
算术关系 | P497 |
赋值 | P499 |
下标 | P501 |
递增递减 | P502 |
成员访问 | P504 |
函数调用 | P506 |
函数重载在各种编程语言中都很常见,使用同一个函数名传递不同的参数(数量或者类型不同)实现不同的功能。
特点:函数名相同,参数(数量或类型)不同。返回值可同可不同。
在Cpp中,函数在编译过程中函数名转化为函数名+参数数量+参数类型
命名的全局函数,同样成员函数也是如此,只是多了类名而已,类名+函数名+参数数量+参数类型
。因此确定一个函数的主要标志就是函数名与参数。
因此,函数名相同,但是参数数量或者类型不同的函数就可以重载,而返回值可以不相同。
int sameFunc(int i, int j) { return i + j; } char sameFunc(int i) { return i; } cout << sameFunc(1, 2) << endl; //3 cout << sameFunc(99) << endl; // 'c'
主要是const形式的重载。
void func(const ObjectFunc& a)
和void func(ObjectFunc& a)
void func()const
和void func()
void func(ObjectFunc& a)
和void func(ObjectFunc &&b)
void func()=0
形式为纯虚函数)Father::func()
在[类和对象之基础]中已经总结过静态函数的笔记。此处主要列出主要的关键字。
::
和点.
构造函数中常见的两种给成员变量赋初值的方式,前一种称为列表初始化,后一种是赋值。实际上,后一种先进行了默认初始化,而后在函数体中执行了拷贝赋值,若遇上const、引用、无默认构造的成员,这种方式就会报错。
ObjectFunc():a('a'), pobj(nullptr) { ... } ObjectFunc(int v) { this->v = v; }
在具体使用中,初始化与赋值虽然都使用等号=,但是初始化是定义的同时初始化,而赋值是给已定义的对象赋值。
总结:成员变量要初始化。尤其遇到const、引用、组合形式的无默认构造的其他对象时,要列表初始化,有指针则要定义拷贝构造、赋值运算符重载、析构函数。
未定义
的错误。因此非静态内置类型变量要初始化,不论是定义时初始化,还是在构造函数函数体前使用列表初始化、还是构造中使用赋值皆可。ObjectFunc(); ObjectFunc obj = ObjectFunc();单独说一下,上述代码并不是仅仅是函数调用,同时还构造了一个临时变量。若此临时变量有其他定义的变量直接承接,相当于临时变量直接转正,不存在拷贝构造函数的调用。
}
结束时进行析构;编译时先编译类的成员变量,而后才编译成员函数,因此即使成员变量定义在成员函数后,成员函数还是能直接引用,而不用声明。
但是使用typedef定义类型时不同,其必须放在类的最初始位置。
先调用父类的构造函数,再调用子类的构造函数;析构时先调用子类的析构函数,而后再调用父类的析构函数。