C++学习笔记-第5单元-对象和类(高级)

C++学习笔记-第5单元

注:本部分内容主要来自中国大学MOOC北京邮电大学崔毅东的《C++程序设计》课程。


第5单元 对象和类(高级)

单元导读

  本单元介绍对象和类的一些高级特性,比如析构函数、委托构造、拷贝构造、深拷贝浅拷贝等。此外还介绍了断言、C++11的常量表达式及静态断言、vector类等内容。

本单元的难点 有两个:
一是静态成员。静态数据成员是由类的所有对象共享的,只有一份。静态数据成员的声明/定义、访问方式都有特殊性。
二是深拷贝和浅拷贝。要理解深浅拷贝这种现象发生的前提是与类的指针成员有关。

本单元的目标 是学完本单元,编程时要会:

  • 用静态数据成员统计对象数量;
  • 用代理构造函数简化代码;
  • 用拷贝构造函数、析构函数;
  • 解决深拷贝的问题;
  • 用assert()调试程序;
  • 知道用vector代替数组。

5.1 [C++11]断言与常量表达式

5.1.1 常量表达式与constexpr关键字

   常量表达式 (Constant expressions)是编译期可以计算值的一个表达式,也就是说编译器来执行这段代码。有时我们需要一个编译期常量(如C++数组的大小),但是 const修饰的对象既可能是编译期常量也可能是运行期常量,这时就需要用到constexpr(只定义编译器常量)。下面的代码展示了const在声明数组时存在的问题:

/* 使用变量声明数组大小:错误 */
int n = 1; 
n ++;
std::array<int, n> a1; // error: n is not a constant expression 
/* 使用const常量声明数组大小:正确 */
const int cn = 2; 
std::array<int, cn> a2; // OK: cn is a constant expression 
/* 使用变量赋值的const常量来声明数组大小:错误 */
const int rcn = n; // rcn is runtime constant, compiler does 
                    // NOT know its value at compile-time  
rcn = ++cn;          // error: rcn is read-only  
std::array<int , rcn> a3; // error: rcn is NOT known at compile-time

  constexpr是C++11中的关键字,称为 编译期常量表达式说明符,constexpr说明符声明可在编译时计算函数或变量的值。下面给出constexpr的使用示例代码:

/* 将max函数变成编译期常量表达式 */
/* 注:这要求所传递的实参都需要时编译期常量 */
constexpr int max(int a , int b) { // c++11 引入 constexpr
   if (a > b) return a;   // c++14才允许constexpr函数中有分支循环等
   else       return b;
}
int main() {
 int m = 1;
 const int rcm = m++;   // 运行期常量
 const int cm = 4;      // 编译期常量,等价于: constexpr int cm = 4;
 int a1[ max(m , rcm)]; // 错误:m & rcm 不是编译期常量
 std::array<char , max(cm , 5)> a2; // OK: cm 和 5 是编译期常量 
}

constconstexpr两者主要的区别是:

const:告知程序员,const 修饰的内容是不会被修改的。主要目的是帮程序员避免bug 。
constexpr:用在所有被要求使用“constant expression”的地方(就是constexpr 修饰的东西可以在编译期计算得到值),主要目的是让编译器能够优化代码提升性能 。

/* 第3、4行体现出const的作用 */
char* s1 = "Hello";       // C语言允许,但C++编译出错
*s1 = 'h';                // C语言中,语法正确,但运行时会出错 
const char* s2 = "World"; // C++ 要求加const
*s2 = 'w';                // C++编译器报错

  另外,constexpr不只能修饰编译器常量表达式,还可以修饰函数、类的构造函数、模板函数等。可以查看C语言中文网 C++11 constexpr:验证是否为常量表达式(长篇神文) 进一步学习。 constexpr用法有非常多的细节(cppreference.com 列出了30 多个条目),且C++14、C++17、C++20都对其有细节上的修改,初学只需简单了解即可。

5.1.2 断言与C++11的静态断言

  断言是一条检测假设成立与否的语句。若假设成立,断言就会悄无声息;若假设不成立,断言就会中断程序的执行。C语言中的断言为assert(),在C++11则定义了静态断言static_assert()。下面分别对其进行介绍。
(1)assert()

  assert是C语言的宏(Macro),运行时检测假设是否成立,注意这个假设不一定是编译期常量表达式。使用时需要调用包含头文件,并以 调试模式 编译程序。调试模式(debug)和发行模型(release)的主要区别在于给编译器传递的编译参数不同(而不是编译的程序不同),比如debug模式会添加额外的附加的用于调试的信息(断点等);而release模型则不会放调试信息,而是会对代码进行编译优化,使代码又小、运行速率又快。下面是用法示例:

#include<cassert> //调试模式编译程序
assert( bool_expr ); // bool_expr 为假则中断程序

std::array a{ 1, 2, 3 };  //C++17 类型参数推导
for (size_t i = 0; i <= a.size(); i++) {
   assert(i < 3);  //断言:i必须小于3,否则失败
   std::cout << a[i];
   std::cout << (i == a.size() ? "" : " ");
}

  注意到assert()依赖于NDEBUG宏(non-debug)。NDEBUG这个宏是C/C++标准规定的,所有编译器都有对它的支持。

(1)调试(Debug)模式编译时,编译器不会定义NDEBUG,所以assert()宏起作用。
(2)发行(Release)模式编译时,编译器自动定义宏NDEBUG,使assert()宏不起作用。
(3)手动输入 #undef NDEBUG,强制使得assert()生效。
(4)手动输入 #define NDEBUG,强制使得assert()不生效。

  assert主要用来帮助调试解决逻辑bug(由于使用“断点/单步调试”可能会很复杂,所以可以部分替代)。当假设不成立时,控制台会显示assrt()括号内的整个语句。下面是一个程序示例:

#undef NDEBUG   // 强制以debug模式使用<cassert>
int main() {
   int i;
   std::cout << "Enter an int: ";
   std::cin >> i;
   assert((i > 0) && "i must be positive"); //注意这里用了与运算符
   return 0;
}

上面示例的第6行代码中,若assert中断了程序则表明程序出bug了!程序员要重编代码解决这个bug,而不是把assert()放在那里当成正常程序的一部分。

  关于何时使用断言assert()这个问题,《代码大全2》:若某些状况是你预期中的,那么用错误处理;若某些状况永不该发生,用断言(Use error-handling for conditions you expect to occur; use assertions for conditions that should never occur.)。

/***可以预料到的问题不使用断言***/
int n{ 1 } , m{ 0 };
std::cin >> n;
assert((n != 0) && "Divisor cannot be zero!"); // 不合适
int q = m / n;

/***预料不到的问题才使用断言***/
int n{ 1 } , m{ 0 };
do {                // 这是修补bug的代码
   std::cin >> n;    // 断言失败后,要解决这个bug
} while (n == 0);   // 在这里编写修复bug的代码
assert((n != 0) && "Divisor cannot be zero!");
int q = m / n;

(2)C++11:static_assert

  static_assert()是一个关键字。C++11中,静态断言的调用语法如下:

static_assert ( bool_constexpr, message)
//1. bool_constexpr: 编译期常量表达式(注意不能有变量),可转换为bool类型
//2. message: 字符串字面量,是断言失败时显示的警告信息。自C++17起,message是可选的。

  static_assert的作用是编译时断言检查,常用在模版编程中,对写库的作者用处大,对于一般的C++程序员几乎用不到。示例如下:

// 下面的语句能够确保该程序在32位的平台上编译进行。
// 如果该程序在64位平台上编译,就会报错 (例子来自MSDN)
static_assert(sizeof(void *) == 4, "64-bit code generation is not supported.");

(3)assert()使用示例

#include<iostream>
#include<array>
#include<cassert>//for using assert

using std::cin;
using std::cout;
using std::endl;
//任务一:用递归计算阶乘factorial,并用assert检查3的阶乘
//任务二:将factorial变成常量表达式,用static_assert检查3 的阶乘
//任务三:创建factorial(4)大小的数组
constexpr int factorial(int n) {
   if (n == 0) {
       return 1;
   }
   else {
       return n * factorial(n - 1);
   }
}
int main() {
   //任务一
   auto f3 = factorial(3);
   assert(f3 == 6 && "3的阶乘应该是6");
   cout << "3! = " << f3 << endl;

   //任务二
   static_assert(factorial(4) == 24, "factorial should be 24");
   //有错误的话,不需要运行就可以直接看到static_assert的波浪线提示
   cout << "4! = " << factorial(4) << endl;
   
   //任务三
   std::array<int, factorial(4)> a;//定义一个数组(要求括号里是编译器常量)
   cout << a.size() << endl;

   cin.get();
   return 0;
}

输出结果:

3! = 6
4! = 24
24

5.1.3 声明与定义

   声明(declare/declaration)是引入一个标识符并描述其类型,无论这个标识符是类型、对象还是函数。编译器需要该“声明”,以便识别在别处使用该标识符。例如:

extern int bar;        //extern声明bar已经在其他的源文件中声明好了
extern int g(int, int);//声明函数
double f(int, double); //声明函数时可以省略extern
class foo;             //声明类时不允许使用extern

   定义(define/definition)用来实例化/实现这个标识符。链接器需要“定义”,以便将对标识符的引用能链接到标识符所表示的实体。例如(注意没有花括号的话都算声明,而不是定义):

int bar;                                    //定义一个实数
int g(int lhs, int rhs) {return lhs*rhs;}   //定义函数体
double f(int i, double d) {return i+d;}     //定义函数体
class foo {};                               //定义一个类

定义与声明的区别(stack overflow文章)主要有三点:

  1. 定义有时可取代声明,反之则不行。
  2. 标识符可被声明多次,但只能定义一次(如下面的例子)。
  3. 定义通常伴随着编译器为标识符分配内存
//多次声明,不会报错
double f(int a, double b);
double f(int a, double b);
//多次定义,会报错
double f(int a, double b){}; 
double f(int a, double b){};

最后总结一下,声明 好像在说:某个地方有个foo;定义 好像在说:它在这儿,长成这样。

5.2 代理构造、不可变对象、静态成员

5.2.1 C++11的代理构造

  代理构造/委托构造(Delegation Constructor)就是一个构造函数可以调用另外的构造函数,好处就是可以少些代码、使得逻辑更加清晰。需要注意的时,一定要避免递归调用目标(环形构造)。下面给出正常的代理构造和递归构造的例子:

/************一、正常的代理构造**********/
class A{
public:   
  A(): A(0){}//被调用构造函数放到主调构造函数的初始化列表位置
  A(int i): A(i, 0){}
  A(int i, int j) {
   num1=i;
   num2=j;
   average=(num1+num2)/2;
  }
  //构造函数的调用次序:A() → A(int) → A(int, int)。
private:
   int num1;
   int num2;
   int average;
};

/***********二、错误的递归构造**********/
class A{
public:   
  A(): A(0){}
  A(int i): A(i, 0){}
  A(int i, int j): A(){}
  //调用次序:A() → A(int) → A(int, int) → A()
private:
  int num1;
  int num2;
  int average;
};

5.2.2 不可变对象

   不可变对象(immutable object)创建后,其内容不可改变,除非通过成员拷贝。不可变对象对于编写多线程程序很有帮助。

若想使类成为“不可变类”,必须以下满足三个条件:

(1)所有数据域均设置为“私有”属性。
(2)没有更改器函数(如seter函数等)。
(3)也没有能够返回可变数据域对象的引用或指针的访问器(下面的代码就是反例)。

int main() {
   //假设已经定义好上面图所示Employee类
   Employee empJack("Jack", Date(1970, 5, 3), Gender::male);
   Date *birthday = empJack.getBirthday();//外部指针指向Employee类里面私有的数据成员
   birthday -> setYear(2010);//改变了Employee类里面私有的数据成员
   cout << "birth year after the change is " <<
           empJack.getBirthDate() -> getYear() << endl;
   return 0;
}

下面给出代码示例:
头文件Date.h

#pragma once//只包含一次头文件
//任务一:创建Date类
#include<iostream>
#include<string>
class Date {
private:
   int year = 2019, month = 1, day = 1;//就地初始化(需要有默认值)
public:
   int getyear() { return year; }
   int getmonth() { return month; }
   int getday() { return day; }
   void setyear(int y) { year = y; }
   void setmonth(int m) { month = m; }
   void setday(int d) { day = d; }
   Date() = default;//没有任何赋值
   Date(int y, int m, int d) :year{ y }, month{ m }, day{ d }{ }
   std::string tostring() {
       return(std::to_string(year) + "-" + std::to_string(month) + "-" + std::to_string(day));
   }//2019-1-1
};

头文件Employee.h

#include<iostream>
#include<string>
#include"Date.h"
//定义gender枚举类型(带有作用域范围的枚举类型)
enum class Gender {
   male,
   famale,
};
//定义employee类
class Employee {
private:
   std::string name;
   Gender gender;
   Date birthday;
public:
   void setname(std::string name) { this->name = name; }
   void setgender(Gender gender) { this->gender = gender; }
   void setbirthday(Date birthday) { this->birthday = birthday; }
   std::string getname() { return name; }
   Gender getgender() { return gender; }
   Date* getbirthday() { return &birthday; }
   std::string tostring() {
       return (name + (gender == Gender::male ? std::string("male/") : std::string("female/"))
           + birthday.tostring());
   }
   //下面是构造函数
   Employee(std::string name, Gender gender, Date birthday) :
       name{ name }, gender{ gender }, birthday{ birthday }{ }//构造函数的初始化列表
   //默认构造函数
   Employee() : Employee("Li yu/", Gender::male, Date(2001, 5, 6)) { }//使用委托构造的形式
};

源文件main.cpp

#include<iostream>
#include"Employee.h"
#include"Date.h"

//创建employee对象,并修改其生日
int main() {
   Employee e;
   //1.setter
   e.setbirthday(Date(1999, 9, 17));
   std::cout << e.tostring() << std::endl;
   //2.getter
   e.getbirthday()->setyear(2000);//拿到的是地址就可以修改
   std::cout << e.tostring() << std::endl;

   std::cin.get();
   return 0;
}

5.2.3 实例成员与静态成员

  静态成员(Static Members)就是加了static的在类中定义的成员。在类的定义中,关键字static声明不绑定到类实例的成员(该成员无需创建对象即可直接使用类的名字进行访问)。静态数据成员定义的规则如下:

(1) 声明为“ constexpr ”类型的静态数据成员必须 在类中声明 并初始化。自C++17 起,不能在类外定义。
(2) 声明为“ inline ”(C++17 起)或者“ const int ”类型的静态数据成员 可以 在类中声明并初始化。
(3) 其它须在 类外定义 并初始化,且不带static关键字。

静态数据成员的定义规则复杂,在类外定义,大部分情况下不会出错,详细的使用方法参见静态成员的说明

  静态数据成员具有静态存储期(static storage duration)或者C++11线程存储期特性。静态存储期指的是对象的存储在程序开始时分配,而在程序结束时解回收。注意静态数据成员的两个特性:
(1) 只存在对象的一个实例
(2) 静态存储器对象未明确初始化时会被自动“零初始化(Zero-Initialization)”

在下面的例子中,一旦实例化了Square(创建了Square的对象),每个对象中都有各自的side成员。这个side成员就叫做实例成员(存在于栈区)。而numberOfObjects作为实例静态成员只存在一个(在全局/静态区),由所有的Square对象共享

class Square {
private:
   double side;//实例成员
   static int numberOfObjects;//静态成员
   // ...
public:
   Square():Square(1.0){ }//无参构造函数
   //有参构造函数
   Square(double side){
       this->side = side;
       numberOfObjects++;
   }
   // ...
};
int Square::numberOfObjects;//静态成员在类外定义,未明确初始化会进行零初始化
int main() {
   Square s1{}, s2{5.0};
}

下面给出测试的代码:
头文件Square.h

#pragma once
//常见square类
class square {
private:
   double side{ 1.0 };         //实例成员
   static int numberofobjects; //静态成员
public:
   square(double side) {
       this->side = side;
       numberofobjects++;
   }
   square() :square(1.0) { }

   double getside() { return side; }
   double getarea() { return side * side; }
   void setside(double side) { this->side = side; }
   static int getnumberofobjects() { return numberofobjects; }
   //使用非静态的函数成员来访问静态的数据成员
   int getnumberofobjectsnonstatic() { return numberofobjects; }
};

源文件main.cpp

//通过类名/对象名调用调用静态成员函数
#include<iostream>
#include"Square.h"

int square::numberofobjects;//静态成员默认初始化为0
int main() {
   square s1;
   //通过对象的名字访问函数成员
   std::cout << s1.getnumberofobjects() << std::endl;

   square s2{ 20.0 };
   //直接使用类的名字+函数成员的名字
   std::cout << square::getnumberofobjects() << std::endl;

   std::cout << s2.getnumberofobjectsnonstatic() << std::endl;
   //std::cout << square::getnumberofobjectsnonstatic() << std::endl;
   //上一行报错说明非静态成员函数不能用类调用
   std::cin.get( );
   return 0;
}

输出结果:

1
2
2

5.3 析构、友元、深浅拷贝

5.3.1 析构函数

  析构函数(dtor)与构造函数(ctor)正好相反。构造函数会在创建对象时自动调用,而析构函数会在函数对象销毁的时候自动调用(使用~C()=delete;可以强制编译器不默认生成析构函数)。注意析构函数没有参数,也没返回值。下面时析构函数与构造函数的对比:

DestructorConstructor
何时调用?对象销毁时对象创建时
原型C::~C( )C::C(arguments)
默认函数的原型C::~C( )C::C( ) 或参数带有默认值
没有显式声明怎么办编译器会生成默认函数编译器会生成默认函数
可否重载?No, only 1Yes

下面给出一个实例示例代码:
头文件Date.h

#pragma once
#include<iostream>
#include<string>
class date {
private:
   int year = 2019, month = 1, day = 1;
public:
   int getyear() { return year; }
   int getmonth() { return month; }
   int getday() { return day; }
   void setyear(int y) { year = y; }
   void setmonth(int m) { month = m; }
   void setday(int d) { day = d; }

   date() = default;
   date(int y, int m, int d) :year{ y }, month{ m }, day{ d }{
       //便于观察date数据的创建和销毁
       std::cout << "date: " << tostring() << std::endl;
   }
   std::string tostring() {
       return(std::to_string(year) + "-" + std::to_string(month) + "-" + std::to_string(day));
   }
};

头文件Employee.h

#include<iostream>
#include<string>
#include"Date.h"
enum class Gender {
   male,
   female,
};
class employee {
private:
   std::string name;
   Gender gender;
   date* birthday;//将雇员类的生日改为date*类型的指针
   static int numberofobjects;//增加静态成员,计算雇员对象的数量
public:
   void setname(std::string name) { this->name = name; }
   void setgender(Gender gender) { this->gender = gender; }
   void setbirthday(date birthday) { *(this->birthday) = birthday; }
   std::string getname() { return name; }
   Gender getgender() { return gender; }
   date getbirthday() { return *birthday; }
   std::string tostring() {
       return (name + (gender == Gender::male ? std::string("male/") : std::string("female/"))
           + birthday->tostring());
   }
   /************************构造函数**************************/
   employee(std::string name, Gender gender, date birthday) :
       name{ name }, gender{ gender }
   {
       numberofobjects++;
       this->birthday = new date{ birthday };//调用了date类的隐式声明的拷贝构造函数
       //new会从堆上进行创建
       std::cout << "Now there are: " << numberofobjects << "employees" << std::endl;
   }
   employee() : employee("Li yu/", Gender::male, date(2001, 5, 6)) { }
   /************************析构函数**************************/
   ~employee() {
       numberofobjects--;
       //在构造函数里声明的对象,就应该在析构函数里归还
       delete birthday;
       birthday = nullptr;
   }
};

源文件main.cpp

#include<iostream>
#include"Date.h"
#include"Employee.h"
int employee::numberofobjects = 0;//记得加作用域employee
//在堆和栈(函数作用域与内嵌作用域)上分别创建employee对象,观察析构函数的行为
int main() {
   employee e1;
   //将主函数的信息都编译打印出来
   std::cout << e1.tostring() << std::endl;

   employee* e2 = new employee{ "Li yu/",Gender::male,date(1999,9,17) };
   std::cout << e2->tostring() << std::endl;

   //大括号表示内嵌作用域,创建了一个栈上的对象(出栈会自动调用析构函数)
   {
       employee e3{ "Tong hua/",Gender::female,{2001,9,16 } };
       std::cout << e3.tostring() << std::endl;
   }
   std::cin.get();
   return 0;
}

输出结果:

date: 2001-5-6
Now there are: 1employees
Li yu/male/2001-5-6
date: 1999-9-17
Now there are: 2employees
Li yu/male/1999-9-17
date: 2001-9-16
Now there are: 3employees
Tong hua/female/2001-9-16

5.3.2 友元函数

  私有成员无法从类外访问,但有时又需要授权某些可信的函数和类访问这些私有成员。这时就需要友元函数(Friend)。使用friend关键字在类内声明友元函数或者友元类。友元函数会给库的编写者带来一些方便,尤其是后期进行运算符重载的时候,当想要重定义流输出运算符的时候,就需要使用友元的方式。友元的缺点是打破了封装性。

下面的例子中,Kid类和print函数都可以直接访问Date类中的私有成员:

class Date {
private:
   int year{ 2019 }, month{ 1 }, day{ 1 };
public:
   friend class Kid;//友元类
   friend void print(const Date& d);//友元函数
};
class Kid {
private:
   Date birthday;
public:
   Kid() { 
       cout << "I was born in " << birthday.year << endl; 
   }
};
void print(const Date& d) {
   cout << d.year << "/" << d.month << "/" << d.day << endl;
}
int main() {
   print(Date());
   Kid k;
   cin.get();
}

5.3.3 拷贝构造函数

  拷贝构造函数是一种特殊的构造函数,可以简写为 copy ctor/ cp ctor。在Unix/Linux中,拷贝文件的命令叫做 cp。拷贝构造(Copy Constructions)的作用是用一个对象初始化另一个同类对象。

/********声明拷贝构造函数********/
Circle (Circle&);//方式一
Circle (const Circle&);//方式二
//例子:带有额外的默认参数的拷贝构造函数
class X {  //来自C++11标准: 12.8节
// ...
public:
   X(const X&, int = 1);
   //只要第一个对象是同类对象的引用,后面的参数都有默认值,那就是拷贝构造函数
};
X b(a, 0); //假设a是一个已有的构造函数
X c = b;   // calls X(const X&, int);

/********调用拷贝构造函数********/
Circle c1( 5.0 ); //先声明一个对象
Circle c2( c1 );    //c++03
Circle c3 = c1;     //c++03,注意不是赋值运算符,而是拷贝构造函数
Circle c5{ c1 };    //c++11
Circle c4 = { c1 }; //c++11

注意上述调用cpoy ctor的例子中,若两个对象obj1和obj2已经定义,那么obj1 = obj2;就不是调用拷贝构造函数,而是对象赋值。反之,若obj2是已经定义好的AClass对象,那么定义对象obj1:AClass obj1 = obj2;的时候,等号(=)就会被解释为初始化,需要调用拷贝构造函数。

  一般情况下,如果程序员不编写拷贝构造函数,那么编译器会自动生成一个。这个自动生成的拷贝构造函数就叫做 隐式声明的拷贝构造函数 (Implicitly-declared Copy Constructor,有时候也叫默认拷贝构造函数)。一般情况下,隐式声明的copy ctor简单地将作为参数的对象中的每个数据域复制到新对象中。

下面是拷贝构造函数的代码示例:
头文件Square.h

#pragma once
//添加拷贝构造函数、析构函数,添加输出信息
#include<iostream>
class square {
private:
   double side{ 1.0 };
   static int numberofobjects;
public:
   double getside() { return side; }
   void setside(double side) { this->side = side; }
   
   //构造函数
   square(double side) {
       this->side = side;
       numberofobjects++;
   }
   square() :square(1.0) { }
   //拷贝构造函数
   square(const square& s) :square(s.side) {//实现委托构造
       //this->side = s.side;
       //numberofobjects++;
       std::cout << "square(const square&) is invoked" << std::endl;
   }
   
   //创建析构函数
   ~square() {
       numberofobjects--;
   }
   
   double getarea() { return side * side; }
   static int getnumberofobjects() { return numberofobjects; }
   //使用非静态的函数成员来访问静态的数据成员
   int getnumberofobjectsnonstatic() { return numberofobjects; }
};

主函数main.cpp

#include<iostream>
#include"Square.h"
//在堆和栈分别拷贝创建square对象
int square::numberofobjects = 0;//注意静态成员一定要初始化
int main() {
   square s1(10.0);//正常创建一个square对象
   std::cout << "square: " << square::getnumberofobjects() << std::endl;//调用静态成员

   square s2{ s1 };//拷贝构造函数
   std::cout << "square: " << square::getnumberofobjects() << std::endl;//调用静态成员
   
   square* s3 = new square{ s1 };//在堆上创建一个square对象,注意是默认调用拷贝构造函数
   std::cout << "square: " << square::getnumberofobjects() << std::endl;//调用静态成员
   std::cout << "s3's area is: " << s3->getarea() << std::endl;         //调用函数
   delete s3;//会调用析构函数
   std::cout << "square: " << square::getnumberofobjects() << std::endl;//调用静态成员

   std::cin.get();
   return 0;
}

运行结果

square: 1
square(const square&) is invoked
square: 2
square(const square&) is invoked
square: 3
s3's area is: 100
square: 2

5.3.4 深拷贝、浅拷贝

  浅拷贝和深拷贝是一个与拷贝构造函数密切相关的问题,注意到只有当数据成员有指针类型才会存在这种问题,否则就没有深/浅拷贝的区别。如果一个类的某个数据域成员是一个指向其他对象的指针,那么 浅拷贝(Shallow Copy) 的意思就是,拷贝构造函数在拷贝的时候会只拷贝指针的地址,而非指针指向的对象/内容(这就会导致在修改指针成员所指向的内容的时候,会影响不止一个类对象)。而 深拷贝(Deep Copy) 的意思就是,拷贝构造函既拷贝非指针类型的成员,也拷贝指针指向的内容。

在两种情况下会出现浅拷贝:

(1) 创建新对象时,调用类的隐式/默认构造函数。
(2) 为已有对象赋值时,使用默认赋值运算符。

图 具有拷贝构造函数和指针类型成员的类

假设某个类如上图所示,那么以下的代码会执行浅拷贝:

Employee e1{"Jack", Date(1999, 5, 3),  Gender::male};
Employee e2{"Anna", Date(2000, 11, 8), Gender::female};
Employee e3{ e1 };  //cp ctor,执行一对一成员拷贝
//上一行代码默认执行浅拷贝,会导致e3.birthday指针指向了e1.birthday所指向的那个Date对象
图 浅拷贝的内存占用

从上图中可以看出,浅拷贝得到的e3,会和e1共用一个Date指针,修改Date的参数会同时影响e1和e3。这时候就需要定制拷贝构造函数(Customizing Copy Construc),也就是深拷贝。

进行深拷贝的步骤如下:

(1) 自行编写拷贝构造函数,不使用编译器隐式生成的(默认)拷贝构造函数。
(2) 重载赋值运算符,不使用编译器隐式生成的(默认)赋值运算符函数。

class Employee {
public:
   // Employee(const Employee &e) = default; //浅拷贝ctor
   Employee(const Employee& e){    //深拷贝ctor
       birthdate = new Date{ e.birthdate };//在堆区拷贝构造出新的外挂对象
   } // ...
}
Employee e1{"Jack", Date(1999, 5, 3), Gender::male};
Employee e2{"Anna", Date(2000, 11, 8),, Gender:female};
Employee e3{ e1 };  //cp ctor 深拷贝
图 深拷贝的内存占用

此时可以看到,进行深拷贝的对象,会独立的占有一个内存,不会影响所拷贝的对象。

下面是对于深浅拷贝的代码验证示例:
头文件Date.h

#pragma once
#include<iostream>
#include<string>
class date {
private:
   int year = 2019, month = 1, day = 1;
public:
   void setyear(int y) { year = y; }
   void setmonth(int m) { month = m; }
   void setday(int d) { day = d; }
   int getyear() { return year; }
   int getmonth() { return month; }
   int getday() { return day; }
   std::string tostring() {
       return(std::to_string(year) + "-" + std::to_string(month) + "-" + std::to_string(day));
   }

   //构造函数
   date() = default;
   date(int y, int m, int d) :year{ y }, month{ m }, day{ d }{
       //便于观察date数据的创建和销毁
       std::cout << "date: " << tostring() << std::endl;
   }
};

头文件Employee.h

//增加静态成员,计算雇员对象的数量
//将雇员类的生日改为date*类型的指针
//修改构造函数和析构函数
#include<iostream>
#include<string>
#include"date.h"
enum class Gender {
   male,
   female,
};
class Employee {
private:
   std::string name;
   Gender gender;
   date* birthday;
   static int numberofobjects;//rmission 1
public:
   void setname(std::string name) { this->name = name; }
   void setgender(Gender gender) { this->gender = gender; }
   void setbirthday(date birthday) { *(this->birthday) = birthday; }
   std::string getname() { return name; }
   Gender getgender() { return gender; }
   date getbirthday() { return *birthday; }
   std::string tostring() {
       return (name + (gender == Gender::male ? std::string("male") : std::string("female"))
           + birthday->tostring());
   }

   /************构造函数*************/
   //调用了date类的拷贝构造函数 
   Employee(std::string name, Gender gender, date birthday) :
       name{ name }, gender{ gender }
   {
       numberofobjects++;
       this->birthday = new date{ birthday };
       std::cout << "Now there are: " << numberofobjects << "employees" << std::endl;
   }
   //默认构造函数
   Employee() : Employee("LiYu ", Gender::male, date(2001, 5, 6)) { }
   //定义深拷贝的拷贝构造函数(没有这个函数就是默认浅拷贝)
   Employee(const Employee& e) : name{ e.name }, gender{ e.gender }{
       numberofobjects++;
       this->birthday = new date{ *(e.birthday) };//调用了date类的拷贝构造函数 
       std::cout << "Now there are: " << numberofobjects << "employees" << std::endl;
   }

   /************析构函数*************/
   ~Employee() {
       numberofobjects--;
       //在构造函数里声明的对象,就应该在析构函数里归还
       delete birthday;
       birthday = nullptr;
   }
};

源文件main.cpp

#include<iostream>
#include"Date.h"
#include"Employee.h"
//任务一:构造employee对象e1,拷贝构造e2
//任务二:调试模式观察e1和e2的birthday成员
//任务三:添加拷贝构造函数实现深拷贝
//任务四:调试模式观察e1和e2的birthday成员
int Employee::numberofobjects = 0;//静态成员的初始化
int main() {
   Employee e1{ "LiYu",Gender::male,{1999,9,17} };//正常创建employee对象
   std::cout << e1.tostring() << std::endl;

   Employee e2{ e1 };//以拷贝构造的形式创建,深/浅拷贝(通过查看地址可以验证)
   std::cout << e2.tostring() << std::endl;

   std::cin.get();
   return 0;
}

运行结果(不是很重要):

date: 1999-9-17
Now there are: 1employees
LiYumale1999-9-17
Now there are: 2employees

5.4 vector类和[C++14]字符串字面量

5.4.1 C++的vector类

  C++的 vector类数组 有些相似的地方,主要的不同是:数组的大小在编译的时候就已经确定好,容量不能够继续改变;而vector对象容量可以随着内容自动增大,比较方便编写一些复杂的程序。一个使用vector的简单例子见下:

vector<int> iV {-2, -1, 0};//列表初始化
// Store numbers 1, ..., 10 to the vector
for (int i = 1; i < 10; i++)
   iV.push_back(i + 1);//在尾部添加函数

vector类还有很多其他的函数,具体见下:

图 vector类的部分函数调用格式

下面给出使用string类和vector类的示例代码:
头文件helper.h

#pragma once
#include<iostream>
#include<vector>
#include<string>
//下面的宏和运算符重载函数可以帮助更快的输出vector的内容
/*代码拷贝地址:https://en.cppreference.com/w/cpp/container/vector/vector*/

//宏定义
#define Print(x) std::cout<<#x<<":"<<x<<std::endl;
//运算符重载函数
template<typename T>
std::ostream& operator<<(std::ostream& s, const std::vector<T>& v) {
   s.put('[');
   char comma[3] = { '\0',' ','\0' };
   for (const auto& e : v) {
       s << comma << e;
       comma[0] = ',';
   }
   return s << ']';
}

源文件main.cpp

#include<iostream>
#include<vector>
#include<string>
#include"helper.h"

int main() {
   //用C++的列表初始化,创建vector对象words
   std::vector<std::string> words1{ "hello","my","beloved","good","morning" };
   Print(words1);

   //删除words最后一个对象
   words1.erase(words1.end() - 1);//.end()获取最后一个元素后面的位置
   Print(words1);

   //在words尾部追加元素
   words1.push_back("afternoon");
   Print(words1);

   //用迭代器拷贝words1的内容以创建word2
   std::vector<std::string> words2(words1.begin() + 3, words1.end());
   Print(words2);

   //在words2中插入元素
   words2.insert(words2.begin(), "hello!");
   Print(words2);

   //用拷贝构造创建words3
   std::vector<std::string> words3(words2);
   Print(words3);

   //用[]修改words3的元素
   words3[2] = "evening";
   Print(words3);

   //创建words4,初始化为多个相同的字串
   std::vector<std::string> words4(4, "Tong hua");
   Print(words4);

   //words3与words4交换
   words3.swap(words4);
   Print(words3);
   Print(words4);

   std::cin.get();
   return 0;
}

运行结果:

words1:[hello, my, beloved, good, morning]
words1:[hello, my, beloved, good]
words1:[hello, my, beloved, good, afternoon]
words2:[good, afternoon]
words2:[hello!, good, afternoon]
words3:[hello!, good, afternoon]
words3:[hello!, good, evening]
words4:[Tong hua, Tong hua, Tong hua, Tong hua]
words3:[Tong hua, Tong hua, Tong hua, Tong hua]
words4:[hello!, good, evening]

5.4.2 C++14的字符串字面量

  C++11 “原始/生”字符串字面量(Raw String literals),其中这个“原始”体现在字符串字面量在程序中写成什么样子,输出之后就是什么样子。我们不需要为“Raw String literals”中的换行、双引号等特殊字符进行转义。使用的语法为 R "delimiter( raw_characters )delimiter",注意两个分隔符(delimiter)要保持一致即可。下面是代码示例:

#include <iostream>
const char* s1 = R"(Hello
World)";
// s1效果与下面的s2和s3相同
const char* s2 = "Hello\nWorld";
const char* s3 = R"NoUse(Hello 
World)NoUse";
int main(){
 std::cout << s1 << std::endl;
 std::cout << s2 << std::endl;
 std::cout << s3 << std::endl;
}

   C++14的字符串字面量 将运算符 ""s进行了重载,赋予了它新的含义,使得用这种运算符括起来的字符串字面量,自动变成了一个 std::string类型的对象(注意要包含std::string头文件)。

auto hello = "Hello!"s;              // hello is of std::string type
auto hello = std::string{"Hello!"};  // equals to the above
auto hello = "Hello!";               // hello is of const char* type

一个来自cppreference.com的例子:

#include <string>
#include <iostream>
int main() {
   using namespace std::string_literals;//为了使用重载运算符""s
   std::string s1 = "abc\0\0def";//注:\0一般作为字符串的结束符
   std::string s2 = "abc\0\0def"s;
   std::cout << "s1: " << s1.size() << " \"" << s1 << "\"\n";
   std::cout << "s2: " << s2.size() << " \"" << s2 << "\"\n";
}

上面例子的一个可能输出结果如下:

s1: 3 "abc"
s2: 8 "abc^@^@def"
//注:下面是我的运行结果
s1: 3 "abc"
s2: 8 "abcdef"

5.5 栈(stack)

   栈(Stack) 是一种后进先出的数据结构。栈的一个明显的应用就是在函数调用时,主函数传给子函数的参数先进栈,进入子函数后,子函数的局部变量也按序在栈中建立;子函数返回时,局部变量出栈、参数出栈。stack类(The Stack Class)封装了栈的存储空间并提供了操作栈的函数。下图展示了栈中各种成员的关系:

图 stack类的成员

下面给出栈类的示例代码:
头文件StackOfIntegers.h

#pragma once
//编写stackofintegers类
class stackofintegers {
private:
   int elements[100];
   int size{ 0 };
public:
   bool empty();
   int peek();
   int push(int value);
   int pop();
   int getsize();
   stackofintegers();
};

源文件StackOfIntegers.cpp

#include"StackOfIntegers.h"
//默认构造函数—私有数据初始化
stackofintegers::stackofintegers() {
   size = 0;
   for (int& i : elements) {//这种方式使得i变成了数组中每一个元素的别名
       i = 0;//将所有的成员都初始化为0
   }
}
//判断栈类是否为空
bool stackofintegers::empty() {
   return(size == 0 ? true : false);
}
//获取已存储元素的数量
int stackofintegers::getsize() {
   return size;
}
//读取不移除栈点的值
int stackofintegers::peek() {
   return elements[size - 1];
}
//将栈点移除并返回它的值
int stackofintegers::pop() {
   int temp = elements[size - 1];
   elements[size - 1] = 0;
   size--;
   return temp;
}
//将value的值放到栈里去
int stackofintegers::push(int value) {
   elements[size] = value;
   size++;
   return value;
}

源文件main.cpp

#include<iostream>
#include"StackOfIntegers.h"
//创建stack对象,展示入栈出栈操作
int main() {
   stackofintegers s1{ };//创建一个栈对象
   /*************入栈操作*****************/
   for (int i = 0; i < 5; i++) {//底 → 1 2 3 4 5 → 顶
       s1.push(i + 1);//将五个元素压入到栈里面
   }
   std::cout << "stack size= " << s1.getsize() << std::endl;
   std::cout << "top element is: " << s1.peek() << std::endl;
   /*************出栈操作*****************/
   const int size = s1.getsize();
   for (int i = 0; i < size; i++) {
       std::cout << s1.pop() << " ";//将五个元素弹出栈
   }
   std::cout << std::endl;
   std::cout << "stack now is empty: " << s1.empty() << std::endl;

   std::cin.get();
   return 0;
}

运行结果:

stack size= 5
top element is: 5
5 4 3 2 1
stack now is empty: 1

5.6 [C++17]结构绑定化

5.6.1 用于数组的结构绑定化

  在C++17中引入的 结构化绑定声明(Structured Binding Declaration) 是一个声明语句,意味着声明了好几个标识符,并同时对这些标识符做了初始化。即,将指定的好几个名字绑定到初始化器的子对象或者元素上。三种调用形态如下:

cv-auto &/&&(可选) [标识符列表] = 表达式;
cv-auto &/&&(可选) [标识符列表] { 表达式 };
cv-auto &/&&(可选) [标识符列表] ( 表达式 );
//cv-auto: 可能由const/volatile修饰的auto关键字。
//&代表左值引用;&&代表右值引用。
//标识符列表:逗号分隔的标识符。

  若初始化表达式为C++原生数组类型,则标识符列表中的名字绑定到数组元素,称为 用于原生数组的结构化绑定声明。注意这种绑定的要求是:

  1. 标识符数量必须等于数组元素数量。
  2. 标识符类型与数组元素类型一致。
int main() {
   int priArr [] {42, 21, 7};
   //ai/bi/ci 的基本类型都是int,只是cv标识或引用标识不同
   auto [a1, a2, a3] = priArr;         // a1 是 priArr[0] 的拷贝,a2, a3类推
   const auto [b1, b2, b3] (priArr);   // b1 是 priArr[0] 的只读拷贝,b2, b3类推
   auto &[c1, c2, c3] {priArr};        // c1 是 priArr[0] 的引用,c2, c3类推
   c3 = 14;                            // priArr[2]的值变为14
   return 0;
}

  若初始化表达式为std::array数组类型,则标识符列表中的名字绑定到数组元素,称为 用于std::array的结构化绑定声明std::array数组类型与原生数组类型的区别在于前者知道数组大小。注意这种绑定的要求是:

  1. 标识符数量必须等于std::array数组中的元素数量
  2. 标识符类型与std::array中的数组元素类型一致
int main() {
   std::array stdArr = {'a','b','c'};//没有写<char, 3>表示使用了模板的自动推导
   auto [d1, d2, d3] {stdArr}; 
   return 0;
}

下面是结构化绑定数组元素的代码示例:
源文件main.cpp

#include<iostream>
#include<array>

int main() {
   //原生数组的结构化绑定
   int a[]{ 1, 2, 3 };//C++原生数组
   auto [e1, e2, e3] = a;//若数量不一致则会报错
   std::cout << e1 << " " << e2 << " " << e3 << std::endl;
   const auto [f1, f2, f3] { a };//注意不能再修改
   //f1 = 10;//不能给常量赋值
   std::cout << f1 << " " << f2 << " " << f3 << std::endl;
   auto &[g1, g2, g3] {a};
   g1 = 5;
   std::cout << a[0] << " " << g2 << " " << g3 << std::endl;
   
   //std::array数组的结构化绑定
   std::array b{ 11, 12, 13 };
   //std::array<int, 2> b{ 4,5 };
   auto [h1, h2, h3] = b;//若数量不一致则会报错
   std::cout << h1 << " " << h2 << " " << h3 << std::endl;
   const auto [i1, i2, i3] { b };//注意不能再修改
   //f1 = 10;//不能给常量赋值
   std::cout << i1 << " " << i2 << " " << i3 << std::endl;
   auto& [k1, k2, k3] { b };
   k1 = 15;
   std::cout << b[0] << " " << k2 << " " << k3 << std::endl;

   std::cin.get();
   return 0;
}

运行结果

1 2 3
1 2 3
5 2 3
11 12 13
11 12 13
15 12 13

5.6.2 用于对象数据成员的结构化绑定声明

  若初始化表达式为类/结构体类型,则标识符列表中的名字绑定到类/结构体的非静态数据成员上,此时称为 用于数据成员的结构化绑定声明。注意这种绑定的要求是:

  1. 数据成员必须为公有成员。注意struct的数据成员默认公有,而class默认私有。
  2. 标识符数量必须等于数据成员的数量。
  3. 标识符类型与数据成员类型一致。
  4. auto后跟&,则标识符是数据成员的引用;auto前可放置const,表明标识符是只读的。
#include<iostream>
class C {  // 可以改用 struct C,然后去掉下面的public属性说明
public:
   int i{ 420 }; // 就地初始化
   char ca[4]{ 'O', 'K', '!', '\0'};//没有\0就会输出烫烫烫
   //注意类中的数组初始化,必须要指明大小
};
int main() {
   C c;
   auto [a1, a2] {c}; // a1是int类型,a2是char[]类型
   std::cout << "a1:" << a1 << std::endl;
   std::cout << "a2:" << a2 << std::endl;

   auto& [b1, b2] { c };// b1是int&类型,是c.i的引用,
                        // b2是char(&)[4]类型(数组的引用), 是c.ca的引用
   a1 = 100;
   std::cout << "c.i:" << c.i << std::endl; // 输出420,改a1不影响c.i
   b1 = 200;
   std::cout << "c.i:" << c.i << std::endl; // 输出200,通过b1修改了c.i
}

结果输出:

a1:420
a2:OK!
c.i:420
c.i:200
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值