类与对象c++(充分认识类及其默认成员函数)
一、认识类1) 类的定义类有两种定义方式1、声明和定义全部放在类体中需注意成员函数如果在类中定义编译器可能会将其当成内联函数处理。2. 类声明放在.h文件中成员函数定义放在.cpp文件中注意成员函数名前需要加类名::一般情况下更期望采用第二种方式因为这样比较体现工程性未来做一些接口的时候也可以打包成动静态库。2)类的访问限定符C实现封装的方式用类将对象的属性与方法结合在一块让对象更加完善通过访问权限选择性的将其接口提供给外部的用户使用。访问限定符1、public:public修饰的成员在类外可以直接被访问2、private:private修饰的成员在类外访问不了3、protected:protected修饰的成员在类外访问不了3类的作用域类定义了一个新的作用域类的所有成员都在类的作用域中。在类体外定义成员时需要使用作用域操作符指明成员属于哪个类域。4类的实例化用类类型创建对象的过程称为类的实例化1、 类是对对象进行描述的是一个模型一样的东西限定了类有哪些成员定义出一个类并没有分配实际的内存空间来存储它比如入学时填写的学生信息表表格就可以看成是一个类来描述具体学生信息。2、一个类可以实例化出多个对象实例化出的对象 占用实际的物理空间存储类成员变量。3、做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子类就像是设计图只设计出需要什么东西但是并没有实体的建筑存在同样类也只是一个设计实例化出的对象才能实际存储数据占用物理空间。7类对象模型类对象的存储方式在类中我们会有成员变量和成员函数那么类的存储方式是怎么样的呢类中并不会存储成员函数只存储成员变量成员函数被放在代码段里因为如果类存储成员函数的话那么我定义了多个自定义变量那么每一个自定义变量都存储着其类的成员函数那么这会导致内存开销大。那么如何计算类的大小呢计算类的大小和计算结构体struct的方法一样1.第一个成员在与结构体偏移量为0的地址处。2.其他成员变量要对齐到某个数字对齐数的整数倍的地址处。注意对齐数 编译器默认的一个对齐数 与 该成员大小的较小值。3.结构体总大小为最大对齐数所有变量类型最大者与默认对齐参数取最小的整数倍。4.如果嵌套了结构体的情况嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数含嵌套结构体的对齐数的整数倍。8this指针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; // 日 }; int main() { Date d1, d2; d1.Init(2022,1,11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }对于上述类有这样的一个问题Date类中有 Init 与 Print 两个成员函数函数体中没有关于不同对象的区分那当d1调用 Init 函数时该函数是如何知道是d1对象调用而不是d2对象调用呢C中通过引入this指针解决该问题即C编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数让该指针指向当前对象(函数运行时调用该函数的对象)在函数体中所有“成员变量”的操作都是通过该指针去访问。只不过所有的操作对用户是透明的即用户不需要来传递编译器自动完成。也就是说在上述代码中有d1.Print(d1)this指针的特性1. this指针的类型类类型* const即成员函数中不能给this指针赋值。2. 只能在“成员函数”的内部使用3. this指针本质上是“成员函数”的形参当对象调用成员函数时将对象地址作为实参传递给this形参。所以对象中不存储this指针。4. this指针是“成员函数”第一个隐含的指针形参一般情况由编译器通过ecx寄存器自动传递不需要用户传递所以说我们在调用类中非静态函数的时候我们要用到类中的成员变量其实是这样的 class Date { public: void Print(Date *this) { cout _year - _month - _day endl; //上面一行等价于:cout this-_year - this-_dat....... } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1 d1.Print(d1); return 0; } 其实看过我那个C多态文章的可以想一下派生类构造出的不同对象其虚函数指针指向的地址都是同一块那如果每个对象的成员变量的值不一样那怎么办呢指向同一块地址能行吗其不同对象存放的要调用的虚函数地址不是一样的吗这时候this指针发力了我们只要传入在对应的虚函数对应的this指针那么就可以实现不同的对象调用其自己的成员变量了。二、类的六个默认成员函数c11之后有8个1构造函数 class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } private: int _year; // 年 int _month; // 月 int _day; // 日 }; int main() { Date d1, d2; d1.Init(2022,1,11); d2.Init(2022, 1, 12); return 0; } 对于这个代码我们每次想要对类的对象里的成员变量赋值都要调用Init共有的函数非常麻烦我们要先创建对象再调用函数那么有没有什么方法可以一次性的创建对象并赋值的呢有的兄弟有的构造函数就可以解决这个问题构造函数特征1. 函数名与类名相同。2. 无返回值。3. 对象实例化时编译器自动调用对应的构造函数。4. 构造函数可以重载。例如class Date { public: // 1.无参构造函数 Date() { } // 2.带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } private: int _year; int _month; int _day; }; void TestDate() { Date d1; // 调用无参构造函数 Date d2(2015, 1, 1); // 调用带参的构造函数 }5.如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。但是编译器自动生成的无参的默认构造函数并不会对成员变量中的内置类型赋值那么这里既然不会对内置类型赋值这编译器自动生成的默认构造函数有什么用呢C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型如int/char...自定义类型就是我们使用class/struct/union等自己定义的类型。而自动生成的构造函数虽然不会处理内置类型但是如果类中有别的自定义类型那么此时默认构造函数会让别的自定义类型调用其默认构造函数。7.无参的构造函数和全缺省的构造函数关于全缺省的构造函数可以看我之前写的一篇博客c类与对象的前置知识都称为默认构造函数并且默认构造函数只能有一个。因为如果无参的和全缺省的构造函数都存在则当调用比如Data类调用Data此时编译错误因为编译器不明白要调用无参的还是全缺省的构造函数。注意无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数都可以认为是默认构造函数。-----总结⼀下就是不传实参就可以调用的构造就叫默认构造。所以我们在编写的时候不能同时存在无参的构造函数和全缺省的构造函数。2析构函数析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数完成对象中资源的清理工作。析构函数的特征1. 析构函数名是在类名前加上字符 ~。2. 无参数无返回值类型。3.一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。注意析构函数不能重载4. 对象生命周期结束时C编译系统系统自动调用析构函数。5.我们没有自定义析构函数的话编译器会自动生成一个默认析构函数同理如果这个类中有其他自定义类型那么此时调用这个类析构函数会把其他自定义的类型析构函数也调用而且析构函数只会销毁每个非静态成员变量本身即释放成员变量所占的栈内存而不会理会这些成员变量可能指向的、在堆上分配的动态资源。所以我们如果类中没有申请动态内存空间可以不用自己写一个构造函数如果申请了那就必须写了否则会造成内存泄漏问题。-其实可以这么理解析构函数值只销毁它内部的变量大小也就是栈上开辟的空间。4拷贝构造函数拷贝构造函数只有单个形参该形参是对本类类型对象的引用(一般常用const修饰)在用已存在的类类型对象创建新对象时由编译器自动调用。注意拷贝构造的形参必须是引用不然不叫拷贝构造而是普通的构造。比如Data d1(d2)此时d1就不是拷贝构造而是普通的构造因为d2是地址传入的是一个指针。拷贝对象的特征1.拷贝构造函数是构造函数的一个重载形式。也就是一种特殊的构造函数同时也会像构造函数一样自动调用自定义成员变量的拷贝构造函数。2.拷贝构造函数的第⼀个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第⼀个参数必须是类类型对象的引用后面的参数必须有缺省值。为什么会引发无穷递归呢比如MyClass objA; MyClass objB(objA); // 调用拷贝构造函数编译器看到Myclass objB(objA)这行代码它知道需要调用拷贝构造函数了为了调用这个函数它需要将实参objA传递给形参other因为是值传递所以这个过程就变成了要构造objB需要调用MyClass(ClassName other)。为了调用这个函数需要先创建形参other。创建other需要调用拷贝构造函数MyClass(ClassName other)。为了调用这个函数需要先创建新的形参other2。创建other2需要调用拷贝构造函数MyClass(ClassName other)。...如此循环永无止境3.若未显式定义编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝这种拷贝叫做浅拷贝或者值拷贝。注意在编译器生成的默认拷贝构造函数中内置类型是按照字节方式直接拷贝的而自定义类型是调用其拷贝构造函数完成拷贝的。4.如果在成员变量有指针的情况下我们必须要用到深拷贝因为浅拷贝是按字节赋值的所以会导致两个指针指向的地址一致一旦二者分别调用析构函数则会报错会导致同一份地址空间被释放两次。5. 拷贝构造函数典型调用场景1使用已存在对象创建新对象2函数参数类型为类类型对象3函数返回值类型为类类型对象6. 传值返回会产生⼀个临时对象调用拷贝构造传值引用返回返回的是返回对象的别名(引用)没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于⼀个野引用类似⼀个野指针⼀样。传引用返回可以减少拷贝但是⼀定要确保返回对象在当前函数结束后还在才能用引用返回。5赋值运算符重载1.赋值运算符重载格式参数类型const T传递引用可以提高传参效率返回值类型T返回引用可以提高返回的效率有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this 要复合连续赋值的含义因为如果没有返回值那就不能与内置类型一样可以连续赋值比如int i,j,p;ijp0这样比如Date operator(const Date d) { if(this ! d) { _year d._year; _month d._month; _day d._day; } return *this; }2.赋值运算符只能重载成类的成员函数不能重载成全局函数。因为如果在全局函数写了一个那么此时类中会发现类内没有赋值运算函数于是编译器会生成一个默认的此时用户再在类外自己实现一个全局的赋值运算符重载就和编译器在类中生成的默认赋值运算符重载冲突了故赋值运算符重载只能是类的成员函数3.用户没有显式实现时编译器会生成一个默认赋值运算符重载以值的方式逐字节拷贝。注意内置类型成员变量是直接赋值的而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。4.同理由于编译器默认生成的赋值运算符重载是以值的方式以字节来浅拷贝那么如果遇到指针类型的话就必须自己实现赋值运算符重载来完成深拷贝了。这里再特别说明一下前置和后置在自定义类型的重载方式C 国际标准ISO/IEC 14882中明确规定了运算符重载的规则对于前缀即a它被重载为没有参数的成员函数operator()。或者作为有一个参数的全局函数operator(ClassType)。对于后缀即a它被重载为有一个int类型参数的成员函数operator(int)。或者作为有两个参数的全局函数operator(ClassType, int)。这个int参数没有任何实际意义它唯一的用途就是在函数重载解析过程中作为一个区分器让编译器能够明确区分这是前缀还是后缀版本的重载。比如class Counter { public: // 前置递增没有参数返回引用 Counter operator() { count; return *this; } // 后置递增有一个int参数返回旧值值传递 Counter operator(int) { Counter old *this; count; return old; } private: int count; };而且我们一般会先实现前置后实现后置因为一些场景中后置可以用前置的实现从而达到类的复合型。6取地址重载主要是普通对象和const对象取地址一般很少自己实现所以这里不做多说不过这个还是有点好处的比如我们可以自己实现一个外部访问不到我们类中成员地址的取地址运算重载。三、再谈构造函数1)初始化列表在构造函数中我们有一个更好也是目前常用的写法初始化列表初始化列表以一个冒号开始接着是一个以逗号分隔的数据成员列表每个成员变量后面跟一个放在括号中的初始值或表达式。注意1.每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)2. 类中包含以下成员必须放在初始化列表位置进行初始化引用成员变量const成员变量自定义类型成员(且该类没有默认构造函数时)3.尽量使用初始化列表初始化因为不管你是否使用初始化列表对于自定义类型成员变量一定会先使用初始化列表初始化。4.成员变量在类中声明次序就是其在初始化列表中的初始化顺序与其在初始化列表中的先后次序无关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 随机值。因为_a2先声明所以初始化_a2先初始化。2)explicit关键字explicit是一个函数说明符主要用于修饰类的构造函数和C11起类型转换函数。它的核心作用是禁止编译器进行非预期的隐式类型转换。举个例子说明一下这个关键字的一些作用class MyString { public: // 没有 explicit 的构造函数 MyString(const char* str) { //初始化操作 std::cout Constructor called with: str std::endl; } void print() const { //不做处理 } }; void displayString(MyString s) { s.print(); } int main() { //这是直观的、我们期望的用法显式构造 MyString s1(Hello); displayString(s1); displayString(World); // 这也是合法的但是是隐式转换 // 编译器会悄悄地、隐式地用字符串字面量构造一个临时的 MyString 对象 //也就是拷贝出一个MyString类型的临时变量传给dispalyString函数 // 然后传递给 displayString 函数 return 0; }如果用explicit关键字修饰构造函数的话class MyString { public: //有explicit的构造函数 explicit MyString(const char* str) { std::cout Constructor called with: str std::endl; } void print() const { //不做处理 } }; void displayString(MyString s) { s.print(); } int main() { MyString s1(Hello); //直接初始化总是允许的显式转换 displayString(s1); //传递一个已存在的对象 // displayString(“World”); //这行不会通过不再能进行隐式转换 // 必须像下面这样显式地转换 displayString(MyString(“World”)); //显式构造 displayString(static_castMyString(C)); //显式转换 return 0; }所以用了explicit关键字我们就无法直接用隐式转换去初始化类。注意 Date d1(1);//构造 Date d2 {2,3}//这里涉及了隐式类型转换如果用explicit关键字修饰了构造函数则无法这样使用 Date d3 d1//拷贝构造函数 还有一个有意思的例子更好理解隐式转换 int i 1; double d i;//这一行是错的因为i要进行隐式类型转换创建一个tmp对象再将tmp对象赋值给d但是临时变量具有常性 //所以要 const double d i;//这样才可以通过 注意虽然临时变量具有常量性而且临时变量很快就会销毁但C有一个特殊规则当将一个临时对象绑定到一个常量引用时该临时对象的生命周期会被延长到与引用的生命周期相同四、static关键字声明为static的类成员称为类的静态成员用static修饰的成员变量称之为静态成员变量用static修饰的成员函数称之为静态成员函数。静态成员变量一定要在类外进行初始化。1 类中的静态成员变量定义与特性声明在类内部但定义在类外部属于类本身而不是类的实例对象,也就是如果有多个类实例化对象其共享这个类中的静态成员变量所有类的实例共享同一个静态成员变量可以通过类名直接访问ClassName::staticMember生命周期贯穿整个程序运行期间2类中的静态成员函数定义与特性静态成员函数可以在没有创建类的任何对象的情况下直接调用也就是不依赖类的实例化静态成员函数没有隐藏的this指针不能访问任何非静态成员但是非静态函数可以访问静态函数静态成员函数可以通过类名直接调用也可以通过对象实例调用但不推荐后者。静态成员函数不能被声明为virtual因为它们不依赖于对象实例而虚函数机制依赖于对象的虚函数表。涉及虚函数知识后续在继承和多态的文章会讲到读者也可以查阅其他有关知识的博客class Logger { public: static void log(const std::string message) { // 记录日志的实现 } }; int main() { Logger::log(System started); // 推荐通过类名调用 Logger logger; logger.log(User logged in); // 不推荐通过对象调用可能引起混淆 return 0; }五、友元友元提供了一种突破封装的方式有时提供了便利。但是友元会增加耦合度破坏了封装所以友元不宜多用。友元分为友元函数和友元类1友元函数友元函数是一个非成员函数但它被授予了访问某个类的私有和保护成员的权限。特点它不是所在类的成员函数它可能是一个全局函数也可能是另一个类的成员函数。它通常在类的内部用friend关键字声明。它的定义在类的外部与普通函数一样不需要使用类名作用域解析运算符。友元函数可以在类定义的任何地方声明不受类访问限定符限制。语法在类的内部声明格式为friend 返回类型 函数名参数列表。例如#include iostream using namespace std; class Point { private: int x, y; public: Point(int x 0, int y 0) : x(x), y(y) {} //声明友元函数 //这个函数不是Point类的成员函数但它能访问Point的私有成员x和y friend Point addPoints(const Point p1, const Point p2); //重载操作符的友元函数也非常常见 friend ostream operator(ostream os, const Point p); }; //友元函数的定义与普通函数一样 Point addPoints(const Point p1, const Point p2) { //可以直接访问私有成员 x 和 y return Point(p1.x p2.x, p1.y p2.y); } //重载操作符 ostream operator(ostream os, const Point p) { os Point( p.x , p.y ); return os; } int main() { Point a(1, 2); Point b(3, 4); Point c addPoints(a, b); // 使用友元函数 cout a b c endl; // 使用重载的 return 0; }那为什么不能在类中实现输入输出流的运算符重载而要在外面呢因为在类中之前提到过this指针this指针都是类中的成员函数的第一个参数除了静态成员变量而当我们要couta的时候编译器第一个解析的是cin的类型是std::istring,然后第二个类型是一个类的类型所以类中重载输入输出流的话我哦们要进行输出只能是a cout;十分不变所以我们要在类外定义让函数参数顺序交换。2友元类友元类是一个类它被授予了访问另一个类的所有私有和保护成员的权限。特点友元关系是单向的。如果类B是类A的友元A中声明friend class B;并不意味着A是B的友元。友元关系不能传递。如果类B是A的友元类C是B的友元这并不意味着C是A的友元。友元关系不能继承。如果类B是A的友元B的派生类并不会自动成为A的友元。语法在类的内部声明格式为friend class 类名。注意虽然友元提供了便利但它破坏了类的封装性这是面向对象设计的一个重要原则。过度使用友元会使代码结构变得混乱增加类之间的耦合度降低可维护性。六、内部类概念如果一个类定义在另一个类的内部这个内部类就叫做内部类。内部类是一个独立的类它不属于外部类更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。注意内部类就是外部类的友元类参见友元类的定义内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。特性1. 内部类可以定义在外部类的public、protected、private都是可以的。2. 注意内部类可以直接访问外部类中的static成员不需要外部类的对象/类名。3. sizeof(外部类)外部类和内部类没有任何关系。例如class A { private: static int k; int h; public: class B // B天生就是A的友元 { public: void foo(const A a) { cout k endl; // OK cout a.h endl; // OK } }; }; int A::k 1; int main() { A::B b; b.foo(A()); return 0; }