C++教程学习笔记总结

C++教程学习笔记总结

目录

从C到C++

C++中的const

在C语言中,const 用来限制一个变量,表示这个变量不能被修改,我们通常称这样的变量为常量。在C语言中,const 用来限制一个变量,表示这个变量不能被修改,我们通常称这样的变量为常量

C++中的 const 更像编译阶段的 #define

  • 示例

    const int m = 10;
    int n = m;
    
    • C语言赋值过程:在C语言中,编译器会先到 m 所在的内存取出一份数据,再将这份数据赋给 n;
    • C++赋值过程:在C++中,编译器会直接将 10 赋给 n,没有读取内存的过程,和int n = 10;的效果一样。C++ 中的常量更类似于#define命令,是一个值替换的过程,只不过#define是在预处理阶段替换,而常量是在编译阶段替换。
  • 总结:C++ 对 const 的处理少了读取内存的过程,优点是提高了程序执行效率,缺点是不能反映内存的变化,一旦 const 变量被修改,C++ 就不能取得最新的值。

  • 修改const变量:const 变量禁止被修改,不过这只是语法层面上的限制,通过指针仍然可以修改。

    如下代码放到.c文件中,以C语言的方式编译,运行结果为99。再将代码放到.cpp文件中,以C++的方式编译,运行结果就变成了10。这种差异正是由于C和C++对 const 的处理方式不同造成的。在C语言中,使用 printf 输出 n 时会到内存中获取 n 的值,这个时候 n 所在内存中的数据已经被修改成了 99,所以输出结果也是 99。而在C++中,printf("%d\n", n);语句在编译时就将 n 的值替换成了 10,效果和printf("%d\n", 10);一样,不管 n 所在的内存如何变化,都不会影响输出结果。

    #include <stdio.h>
    int main(){
        const int n = 10;
        int *p = (int*)&n;  //必须强制类型转换。&n得到的指针的类型是const int *,必须强制转换为int *后才能赋给 p,否则类型是不兼容的。
        *p = 99;  //修改const变量的值
        printf("%d\n", n);
        return 0;
    }
    

C++中全局 const 变量的可见范围是当前文件

普通全局变量的作用域是当前文件,但是在其他文件中也是可见的,使用extern声明后就可以使用

  • 示例:不管是以C还是C++的方式编译,运行结果都是module: 10 main: 10

    • 源文件1:

      #include <stdio.h>
      int n = 10;
      void func();
      int main(){
          func();
          printf("main: %d\n", n);
          return 0;
      }
      
    • 源文件2:

      #include <stdio.h>
      extern int n;
      void func(){
          printf("module: %d\n", n);
      }
      
  • C语言执行效果:在C语言中,const 变量和普通变量一样,在其他源文件中也是可见的。修改代码段1,在 n 的定义前面加 const 限制,运行结果和上面也是一样的。这说明C语言中的 const 变量在多文件编程时的表现和普通变量一样,除了不能修改,没有其他区别。

  • C++执行效果:按照C++的方式编译(将源文件后缀设置为.cpp),修改后的代码就是错误的。这是因为 C++ 对 const 的特性做了调整,C++ 规定,全局 const 变量的作用域仍然是当前文件,但是它在其他文件中是不可见的,这和添加了static关键字的效果类似。虽然代码段2中使用 extern 声明了变量 n,但是在链接时却找不到代码段1中的 n。

  • 总结:C和C++中全局 const 变量的作用域相同,都是当前文件,不同的是它们的可见范围:C语言中 const 全局变量的可见范围是整个程序,在其他文件中使用 extern 声明后就可以使用;而C++中 const 全局变量的可见范围仅限于当前文件,在其他文件中不可见,所以它可以定义在头文件中,多次引入后也不会出错。

C++ new和delete运算符

  • C语言动态分配内存:

    int *p = (int*) malloc( sizeof(int) * 10 );  //分配10个int型的内存空间
    free(p);  //释放内存
    
  • C++动态分配内存:

    • new 操作符会根据后面的数据类型来推断所需空间的大小。
    int *p = new int;  //分配1个int型的内存空间
    delete p;  //释放内存
    
    • 分配一组连续的数据
    int *p = new int[10];  //分配10个int型的内存空间
    delete[] p;
    

C++函数的默认参数

C++规定,默认参数只能放在形参列表的最后,而且一旦为某个形参指定了默认值,那么它后面的所有形参都必须有默认值。

函数的定义和声明若位于同一个源文件,那么它们的作用域也都是整个源文件,这样就导致在同一个文件作用域中指定了两次默认参数,违反了 C++ 的规定。若声明处与定义处默认参数的值不同,编译器优先使用的是当前作用域中的默认参数。

  • 示例:

    #include <iostream>
    using namespace std;
    void func(int a, int b = 10, int c = 36);
    int main(){
        func(99);
        return 0;
    }
    // 报错,错误信息表明不能在函数定义和函数声明中同时指定默认参数。
    void func(int a, int b = 10, int c = 36){
        cout<<a<<", "<<b<<", "<<c<<endl;
    }
    

C++和C的混合编程

C 语言是不支持函数重载的,根据 C++ 标准编译后的函数名几乎都由原有函数名和各个参数的数据类型构成,而根据 C 语言标准编译后的函数名则仅有原函数名构成。

  • 示例:当 myfun.h 被引入到 C++ 程序中时,会选择带有 extern "C" 修饰的 display() 函数;反之如果 myfun.h 被引入到 C 语言程序中,则会选择不带 extern "C" 修饰的 display() 函数。由此,无论 display() 函数位于 C++ 程序还是 C 语言程序,都保证了 display() 函数可以按照 C 语言的标准来处理。

    //myfun.h
    void display();
    
    //myfun.c
    #include <stdio.h>
    #include "myfun.h"
    void display(){
       printf("C++:http://c.biancheng/net/cplus/");
    }
    
    //main.cpp
    #include <iostream>
    #include "myfun.h"
    using namespace std;
    int main(){
       display();
       return 0;
    }
    
  • extern "C"

    • 示例1:

      #ifdef __cplusplus
      extern "C" void display();
      #else
      void display();
      #endif
      
    • 示例2:

      #ifdef __cplusplus
      extern "C" {
      #endif
      void display();
      #ifdef __cplusplus
      }
      #endif
      

类和对象

C++类的成员变量和成员函数

  • 类体中和类体外定义成员函数的区别:在类体中定义的成员函数会自动成为内联函数,在类体外定义的不会。

C++成员对象和封闭类

一个类的成员变量如果是另一个类的对象,就称之为“成员对象”。包含成员对象的类叫封闭类。

  • 成员对象的消亡:
    • 构造:先执行成员对象的构造函数,然后执行封闭类的构造函数。
    • 析构:先执行封闭类的析构函数,然后执行成员对象的析构函数。

C++ this指针

  • this指针实质:
    • this 实际上是成员函数的一个形参,在调用成员函数时将对象的地址作为实参传递给 this。不过 this 这个形参是隐式的,它并不出现在代码中,而是在编译阶段由编译器默默地将它添加到参数列表中。
    • this 作为隐式形参,本质上是成员函数的局部变量,所以只能用在成员函数的内部,并且只有在通过对象调用成员函数时才给 this 赋值。
    • 成员函数最终被编译成与对象无关的普通函数,除了成员变量,会丢失所有信息,所以编译时要在成员函数中添加一个额外的参数,把当前对象的首地址传入,以此来关联成员函数和成员变量。这个额外的参数,实际上就是 this,它是成员函数和成员变量关联的桥梁。

C++ static静态成员变量

  • 一个类中可以有一个或多个静态成员变量,所有的对象都共享这些静态成员变量,都可以引用它。
  • static 成员变量和普通 static 变量一样,都在内存分区中的全局数据区分配内存,到程序结束时才释放。这就意味着,static 成员变量不随对象的创建而分配内存,也不随对象的销毁而释放内存。而普通成员变量在对象创建时分配内存,在对象销毁时释放内存。
  • 静态成员变量必须初始化,而且只能在类体外进行。例如:int Student::m_total = 10
  • 静态成员变量既可以通过对象名访问,也可以通过类名访问,但要遵循 private、protected 和 public 关键字的访问权限限制。当通过对象名访问时,对于不同的对象,访问的是同一份内存。

C++ static静态成员函数

  • 静态成员函数与普通成员函数的根本区别:

    • 普通成员函数有 this 指针,可以访问类中的任意成员;
    • 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
  • 静态成员函数使用:int total = Student::getTotal()

C++ const成员变量和成员函数

  • 函数开头的 const 用来修饰函数的返回值,表示返回值是 const 类型,也就是不能被修改,例如const char * getname()
  • 函数头部的结尾加上 const 表示常成员函数,这种函数只能读取成员变量的值,而不能修改成员变量的值,例如char * getname() const

C++ const对象

  • 一旦将对象定义为常对象之后,不管是哪种形式,该对象就只能访问被 const 修饰的成员了(包括 const 成员变量和 const 成员函数),因为非 const 成员可能会修改对象的数据(编译器也会这样假设),C++禁止这样做。

C++友元函数和友元类(C++ friend关键字)

  • 友元函数不同于类的成员函数,在友元函数中不能直接访问类的成员,必须要借助对象。下面的写法是错误的:

    void show(){
        cout<<m_name<<"的年龄是 "<<m_age<<",成绩是 "<<m_score<<endl;
    }
    
  • 友元类:

    1. 一般不建议把整个类声明为友元类,而只将某些成员函数声明为友元函数,这样更安全一些。
    2. 友元类中的所有成员函数都是另外一个类的友元函数。

C++ class和struct的区别

  • C++中的 struct 和 class 基本是通用的,唯有几个细节不同:
    • 使用 class 时,类中的成员默认都是 private 属性的;而使用 struct 时,结构体中的成员默认都是 public 属性的。
    • class 继承默认是 private 继承,而 struct 继承默认是 public 继承。
    • class 可以使用模板,而 struct 不能。

C++多态与虚函数

C++多态与虚函数快速入门

#include <iostream>
using namespace std;

//基类People
class People {
public:
	People(const char *name, int age);
	virtual void display(); // 在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
protected:
	const char *m_name;
	int m_age;
};

People::People(const char *name, int age) : m_name(name), m_age(age) {}

void People::display() {
	cout << m_name << "今年" << m_age << "岁了,是个无业游民。" << endl;
}

//派生类Teacher
class Teacher : public People {
public:
	Teacher(const char *name, int age, int salary);
	void display();
private:
	int m_salary;
};


Teacher::Teacher(const char *name, int age, int salary) : People(name, age), m_salary(salary) {}
void Teacher::display() {
	cout << m_name << "今年" << m_age << "岁了,是一名教师,每月有" << m_salary << "元的收入。" << endl;
}

int main() {
	People *p = new People("王志刚", 23);
	p->display();

	// 基类指针指向派生类对象
	// 通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。
	// 派生类比较多,如果不使用多态,那么就需要定义多个指针变量,很容易造成混乱;而有了多态,只需要一个指针变量 p 就可以调用所有派生类的虚函数。
	p = new Teacher("赵宏佳", 45, 8200);
	p->display();

	return 0;
}

C++虚函数注意事项以及构成多态的条件

  • 虚函数的注意事项

    1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
    2. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为virtual void func();,派生类虚函数的原型为virtual void func(int);,那么当基类指针 p 指向派生类对象时,语句p -> func(100);将会出错,而语句p -> func();将调用基类的函数。
    3. 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。另外,C++ 中的构造函数用于在创建对象时进行初始化工作,在执行构造函数之前对象尚未创建完成,虚函数表尚不存在,也没有指向虚函数表的指针,所以此时无法查询虚函数表,也就不知道要调用哪一个构造函数。
    4. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数。
  • 构成多态的条件

    1. 必须存在继承关系;
    2. 继承关系中必须有同名的虚函数,并且它们是覆盖关系(函数原型相同)。
    3. 存在基类的指针,通过该指针调用虚函数。
  • 使用虚函数的时机

    1. 首先看成员函数所在的类是否会作为基类。
    2. 然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。
#include <iostream>
using namespace std;

//基类Base
class Base {
public:
	virtual void func();
	virtual void func(int);
};
void Base::func() {
	cout << "void Base::func()" << endl;
}
void Base::func(int n) {
	cout << "void Base::func(int)" << endl;
}

//派生类Derived
class Derived : public Base {
public:
	void func();
	void func(char *);
};
void Derived::func() {
	cout << "void Derived::func()" << endl;
}
void Derived::func(char *str) {
	cout << "void Derived::func(char *)" << endl;
}

int main() {
	Base *p = new Derived();
	p->func();  // 调用的是派生类的虚函数,构成了多态。
	p->func(10);  // 调用的是基类的虚函数,因为派生类中没有函数覆盖它。
	p->func("http://c.biancheng.net");  // 出现编译错误,因为通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员。

	return 0;
}

C++虚析构函数的必要性

#include <iostream>
using namespace std;

//基类
class Base {
public:
	Base();
	~Base();
protected:
	char *str;
};
Base::Base() {
	str = new char[100];
	cout << "Base constructor" << endl;
}
Base::~Base() {
	delete[] str;
	cout << "Base destructor" << endl;
}

//派生类
class Derived : public Base {
public:
	Derived();
	~Derived();
private:
	char *name;
};
Derived::Derived() {
	name = new char[100];
	cout << "Derived constructor" << endl;
}
Derived::~Derived() {
	delete[] name;
	cout << "Derived destructor" << endl;
}

int main() {
	// 因为这里的析构函数是非虚函数,通过指针访问非虚函数时,编译器会根据指针的类型来确定要调用的函数;
	// 也就是说,指针指向哪个类就调用哪个类的函数,这在前面的章节中已经多次强调过。
	// pb 是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。
	Base *pb = new Derived();
	delete pb;

	cout << "-------------------" << endl;
	// pd 是派生类的指针,编译器会根据它的类型匹配到派生类的析构函数,
	// 在执行派生类的析构函数的过程中,又会调用基类的析构函数。
	// 派生类析构函数始终会调用基类的析构函数,并且这个过程是隐式完成的
	Derived *pd = new Derived();
	delete pd;

	return 0;
}

C++纯虚函数和抽象类详解

  • 纯虚函数virtual 返回值类型 函数名 (函数参数) = 0;
  • 抽象类:包含纯虚函数的类称为抽象类。抽象类无法实例化,也就是无法创建对象。因为纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
#include <iostream>
using namespace std;

//线
class Line {
public:
	Line(float len);
	virtual float area() = 0;
	virtual float volume() = 0;
protected:
	float m_len;
};
Line::Line(float len) : m_len(len) { }

//矩形
class Rec : public Line {
public:
	Rec(float len, float width);
	float area();
protected:
	float m_width;
};
Rec::Rec(float len, float width) : Line(len), m_width(width) { }
float Rec::area() { return m_len * m_width; }

//长方体
class Cuboid : public Rec {
public:
	Cuboid(float len, float width, float height);
	float area();
	float volume();
protected:
	float m_height;
};
Cuboid::Cuboid(float len, float width, float height) : Rec(len, width), m_height(height) { }
float Cuboid::area() { return 2 * (m_len*m_width + m_len * m_height + m_width * m_height); }
float Cuboid::volume() { return m_len * m_width * m_height; }

//正方体
class Cube : public Cuboid {
public:
	Cube(float len);
	float area();
	float volume();
};
Cube::Cube(float len) : Cuboid(len, len, len) { }
float Cube::area() { return 6 * m_len * m_len; }
float Cube::volume() { return m_len * m_len * m_len; }

int main() {
	Line *p = new Cuboid(10, 20, 30);
	cout << "The area of Cuboid is " << p->area() << endl;
	cout << "The volume of Cuboid is " << p->volume() << endl;

	p = new Cube(15);
	cout << "The area of Cube is " << p->area() << endl;
	cout << "The volume of Cube is " << p->volume() << endl;

	return 0;
}

C++虚函数表,直戳多态的实现机制

  • 观察虚函数表结论
    1. 基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable 的最后。
    2. 如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable 中只会出现一次。
image-20230717095856409
  • 虚函数调用实质:当通过指针调用虚函数时,先根据类对象指针p找到 vfptr,再根据 vfptr 找到虚函数的入口地址。
    1. 以虚函数 display() 为例,它在 vtable 中的索引为 0,通过 p 调用时:p -> display();
    2. 编译器内部会发生类似如下的转换:( *( *(p+0) + 0 ) )(p);。eating()会发生类似转换:( *( *(p+0) + 1 ) )(p);
      • 0是 vfptr 在对象中的偏移,p+0是 vfptr 的地址;
      • *(p+0)是 vfptr 的值,而 vfptr 是指向 vtable 的指针,所以*(p+0)也就是 vtable 的地址;
      • display() 在 vtable 中的索引(下标)是 0,所以( *(p+0) + 0 )也就是 display() 的地址;
      • 知道了 display() 的地址,( *( *(p+0) + 0 ) )(p)也就是对 display() 的调用了,这里的 p 就是传递的实参,它会赋值给 this 指针。
#include <iostream>
#include <string>
using namespace std;

//People类
class People {
public:
	People(string name, int age);
public:
	virtual void display();
	virtual void eating();
protected:
	string m_name;
	int m_age;
};
People::People(string name, int age) : m_name(name), m_age(age) { }
void People::display() {
	cout << "Class People:" << m_name << "今年" << m_age << "岁了。" << endl;
}
void People::eating() {
	cout << "Class People:我正在吃饭,请不要跟我说话..." << endl;
}

//Student类
class Student : public People {
public:
	Student(string name, int age, float score);
public:
	virtual void display();
	virtual void examing();
protected:
	float m_score;
};
Student::Student(string name, int age, float score) :
	People(name, age), m_score(score) { }
void Student::display() {
	cout << "Class Student:" << m_name << "今年" << m_age << "岁了,考了" << m_score << "分。" << endl;
}
void Student::examing() {
	cout << "Class Student:" << m_name << "正在考试,请不要打扰T啊!" << endl;
}

//Senior类
class Senior : public Student {
public:
	Senior(string name, int age, float score, bool hasJob);
public:
	virtual void display();
	virtual void partying();
private:
	bool m_hasJob;
};
Senior::Senior(string name, int age, float score, bool hasJob) :
	Student(name, age, score), m_hasJob(hasJob) { }
void Senior::display() {
	if (m_hasJob) {
		cout << "Class Senior:" << m_name << "以" << m_score << "的成绩从大学毕业了,并且顺利找到了工作,Ta今年" << m_age << "岁。" << endl;
	}
	else {
		cout << "Class Senior:" << m_name << "以" << m_score << "的成绩从大学毕业了,不过找工作不顺利,Ta今年" << m_age << "岁。" << endl;
	}
}
void Senior::partying() {
	cout << "Class Senior:快毕业了,大家都在吃散伙饭..." << endl;
}

int main() {
	People *p = new People("赵红", 29);
	p->display();

	p = new Student("王刚", 16, 84.5);
	p->display();

	p = new Senior("李智", 22, 92.0, true);
	p->display();

	return 0;
}

C++ typeid运算符:获取类型信息

  • typeid 运算符用来获取一个表达式的类型信息。不像 Java、C# 等动态性较强的语言,C++ 能获取到的类型信息非常有限,也没有统一的标准,如同“鸡肋”一般,大部分情况下我们只是使用重载过的“==”运算符来判断两个类型是否相同。
#include <iostream>
#include <typeinfo>
using namespace std;

class Base { };

struct STU { };

int main() {
	//获取一个普通变量的类型信息
	int n = 100;
	const type_info &nInfo = typeid(n);
	cout << nInfo.name() << " | " << nInfo.raw_name() << " | " << nInfo.hash_code() << endl;

	//获取一个字面量的类型信息
	const type_info &dInfo = typeid(25.65);
	cout << dInfo.name() << " | " << dInfo.raw_name() << " | " << dInfo.hash_code() << endl;

	//获取一个对象的类型信息
	Base obj;
	const type_info &objInfo = typeid(obj);
	cout << objInfo.name() << " | " << objInfo.raw_name() << " | " << objInfo.hash_code() << endl;

	//获取一个类的类型信息
	const type_info &baseInfo = typeid(Base);
	cout << baseInfo.name() << " | " << baseInfo.raw_name() << " | " << baseInfo.hash_code() << endl;

	//获取一个结构体的类型信息
	const type_info &stuInfo = typeid(struct STU);
	cout << stuInfo.name() << " | " << stuInfo.raw_name() << " | " << stuInfo.hash_code() << endl;

	//获取一个普通类型的类型信息
	const type_info &charInfo = typeid(char);
	cout << charInfo.name() << " | " << charInfo.raw_name() << " | " << charInfo.hash_code() << endl;

	//获取一个表达式的类型信息
	const type_info &expInfo = typeid(20 * 45 / 4.5);
	cout << expInfo.name() << " | " << expInfo.raw_name() << " | " << expInfo.hash_code() << endl;

	return 0;
}

C++ RTTI机制精讲(C++运行时类型识别机制)

多态是面向对象编程的一个重要特征,它极大地增加了程序的灵活性,C++、C#、Java 等“正统的”面向对象编程语言都支持多态。但是支持多态的代价也是很大的,有些信息在编译阶段无法确定下来,必须提前做好充足的准备,让程序运行后再执行一段代码获取,这会消耗更多的内存和 CPU 资源。

#include <iostream>
using namespace std;

//基类
class People{
public:
    virtual void func(){ }
};

//派生类
class Student: public People{ };

int main(){
    People *p;
    int n;
  
    cin>>n;
    if(n <= 100){
        p = new People();
    }else{
        p = new Student();
    }

    //根据不同的类型进行不同的操作
    if(typeid(*p) == typeid(People)){
        cout<<"I am human."<<endl;
    }else{
        cout<<"I am a student."<<endl;
    }

    return 0;
}

C++静态绑定和动态绑定,彻底理解多态

C++ 是一门静态性的语言,会尽力在编译期间找到函数的地址,以提高程序的运行效率

  • 符号绑定:将变量名和函数名统称为符号,找到符号对应的地址的过程叫做符号绑定。

  • 函数绑定:找到函数名对应的地址,然后将函数调用处用该地址替换,这称为函数绑定。

  • 静态绑定:一般情况下,在编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定,程序运行后直接使用这个地址即可。这称为静态绑定。

  • 动态绑定:有时候在编译期间不能确定使用哪个函数,必须要等到程序运行后根据具体的环境或者用户操作才能决定。这称为动态绑定。

运算符重载

C++运算符重载基础

运算符重载是通过函数重载实现的

#include <iostream>
using namespace std;

class complex {
public:
	complex();
	complex(double real, double imag);
public:
	//声明运算符重载
	complex operator+(const complex &A) const;
	void display() const;
private:
	double m_real;  //实部
	double m_imag;  //虚部
};

complex::complex() : m_real(0.0), m_imag(0.0) { }
complex::complex(double real, double imag) : m_real(real), m_imag(imag) { }

//实现运算符重载
complex complex::operator+(const complex &A) const {
	complex B;
	B.m_real = this->m_real + A.m_real;
	B.m_imag = this->m_imag + A.m_imag;
	return B;
}

void complex::display() const {
	cout << m_real << " + " << m_imag << "i" << endl;
}

int main() {
	complex c1(4.3, 5.8);
	complex c2(2.4, 3.7);
	complex c3;
	c3 = c1 + c2;
	c3.display();

	return 0;
}

C++运算符重载时要遵循的规则

  • 注意事项
    1. 不是所有的运算符都可以重载。大部分运算符都能够重载,长度运算符sizeof、条件运算符: ?、成员选择符.和域解析运算符::不能被重载。
    2. 重载不能改变运算符的优先级和结合性。
    3. 重载不会改变运算符的用法,原来有几个操作数、操作数在左边还是在右边,这些都不会改变。例如~号右边只有一个操作数,+号总是出现在两个操作数之间,重载后也必须如此。
    4. 运算符重载函数不能有默认的参数,否则就改变了运算符操作数的个数。将运算符重载函数作为类的成员函数时,二元运算符的参数只有一个,一元运算符不需要参数。
    5. 运算符重载函数既可以作为类的成员函数,也可以作为全局函数。但箭头运算符->、下标运算符[ ]、函数调用运算符( )、赋值运算符=只能以成员函数的形式重载。

C++重载数学运算符

#include <iostream>
#include <cmath>
using namespace std;

//复数类
class Complex {
public:  //构造函数
	Complex(double real = 0.0, double imag = 0.0) : m_real(real), m_imag(imag) { }
public:  //运算符重载
	//以全局函数的形式重载
	friend Complex operator+(const Complex &c1, const Complex &c2);
	friend Complex operator-(const Complex &c1, const Complex &c2);
	friend Complex operator*(const Complex &c1, const Complex &c2);
	friend Complex operator/(const Complex &c1, const Complex &c2);
	friend bool operator==(const Complex &c1, const Complex &c2);
	friend bool operator!=(const Complex &c1, const Complex &c2);
	//以成员函数的形式重载
	Complex & operator+=(const Complex &c);
	Complex & operator-=(const Complex &c);
	Complex & operator*=(const Complex &c);
	Complex & operator/=(const Complex &c);
public:  //成员函数
	double real() const { return m_real; }
	double imag() const { return m_imag; }
private:
	double m_real;  //实部
	double m_imag;  //虚部
};

//重载+运算符
Complex operator+(const Complex &c1, const Complex &c2) {
	Complex c;
	c.m_real = c1.m_real + c2.m_real;
	c.m_imag = c1.m_imag + c2.m_imag;
	return c;
}
//重载-运算符
Complex operator-(const Complex &c1, const Complex &c2) {
	Complex c;
	c.m_real = c1.m_real - c2.m_real;
	c.m_imag = c1.m_imag - c2.m_imag;
	return c;
}
//重载*运算符  (a+bi) * (c+di) = (ac-bd) + (bc+ad)i
Complex operator*(const Complex &c1, const Complex &c2) {
	Complex c;
	c.m_real = c1.m_real * c2.m_real - c1.m_imag * c2.m_imag;
	c.m_imag = c1.m_imag * c2.m_real + c1.m_real * c2.m_imag;
	return c;
}
//重载/运算符  (a+bi) / (c+di) = [(ac+bd) / (c²+d²)] + [(bc-ad) / (c²+d²)]i
Complex operator/(const Complex &c1, const Complex &c2) {
	Complex c;
	c.m_real = (c1.m_real*c2.m_real + c1.m_imag*c2.m_imag) / (pow(c2.m_real, 2) + pow(c2.m_imag, 2));
	c.m_imag = (c1.m_imag*c2.m_real - c1.m_real*c2.m_imag) / (pow(c2.m_real, 2) + pow(c2.m_imag, 2));
	return c;
}
//重载==运算符
bool operator==(const Complex &c1, const Complex &c2) {
	if (c1.m_real == c2.m_real && c1.m_imag == c2.m_imag) {
		return true;
	}
	else {
		return false;
	}
}
//重载!=运算符
bool operator!=(const Complex &c1, const Complex &c2) {
	if (c1.m_real != c2.m_real || c1.m_imag != c2.m_imag) {
		return true;
	}
	else {
		return false;
	}
}

//重载+=运算符
Complex & Complex::operator+=(const Complex &c) {
	this->m_real += c.m_real;
	this->m_imag += c.m_imag;
	return *this;
}
//重载-=运算符
Complex & Complex::operator-=(const Complex &c) {
	this->m_real -= c.m_real;
	this->m_imag -= c.m_imag;
	return *this;
}
//重载*=运算符
Complex & Complex::operator*=(const Complex &c) {
	this->m_real = this->m_real * c.m_real - this->m_imag * c.m_imag;
	this->m_imag = this->m_imag * c.m_real + this->m_real * c.m_imag;
	return *this;
}
//重载/=运算符
Complex & Complex::operator/=(const Complex &c) {
	this->m_real = (this->m_real*c.m_real + this->m_imag*c.m_imag) / (pow(c.m_real, 2) + pow(c.m_imag, 2));
	this->m_imag = (this->m_imag*c.m_real - this->m_real*c.m_imag) / (pow(c.m_real, 2) + pow(c.m_imag, 2));
	return *this;
}

int main() {
	Complex c1(25, 35);
	Complex c2(10, 20);
	Complex c3(1, 2);
	Complex c4(4, 9);
	Complex c5(34, 6);
	Complex c6(80, 90);

	Complex c7 = c1 + c2;
	Complex c8 = c1 - c2;
	Complex c9 = c1 * c2;
	Complex c10 = c1 / c2;
	cout << "c7 = " << c7.real() << " + " << c7.imag() << "i" << endl;
	cout << "c8 = " << c8.real() << " + " << c8.imag() << "i" << endl;
	cout << "c9 = " << c9.real() << " + " << c9.imag() << "i" << endl;
	cout << "c10 = " << c10.real() << " + " << c10.imag() << "i" << endl;

	c3 += c1;
	c4 -= c2;
	c5 *= c2;
	c6 /= c2;
	cout << "c3 = " << c3.real() << " + " << c3.imag() << "i" << endl;
	cout << "c4 = " << c4.real() << " + " << c4.imag() << "i" << endl;
	cout << "c5 = " << c5.real() << " + " << c5.imag() << "i" << endl;
	cout << "c6 = " << c6.real() << " + " << c6.imag() << "i" << endl;

	if (c1 == c2) {
		cout << "c1 == c2" << endl;
	}
	if (c1 != c2) {
		cout << "c1 != c2" << endl;
	}

	return 0;
}

到底以成员函数还是全局函数(友元函数)的形式重载运算符

运算符重载的初衷是给类添加新的功能,方便类的运算,它作为类的成员函数是理所应当的,是首选的。不过,类的成员函数不能对称地处理数据,程序员必须在(参与运算的)所有类型的内部都重载当前的运算符。

  • 转换构造函数:如下示例,Complex(double real);在作为普通构造函数的同时,还能将 double 类型转换为 Complex 类型,集合了“构造函数”和“类型转换”的功能,所以被称为「转换构造函数」。换句话说,转换构造函数用来将其它类型(可以是 bool、int、double 等基本类型,也可以是数组、指针、结构体、类等构造类型)转换为当前类类型。

    #include <iostream>
    using namespace std;
    
    //复数类
    class Complex {
    public:
    	Complex() : m_real(0.0), m_imag(0.0) { }
    	Complex(double real, double imag) : m_real(real), m_imag(imag) { }
    	Complex(double real) : m_real(real), m_imag(0.0) { }  //转换构造函数
    public:
    	friend Complex operator+(const Complex &c1, const Complex &c2);
    public:
    	double real() const { return m_real; }
    	double imag() const { return m_imag; }
    private:
    	double m_real;  //实部
    	double m_imag;  //虚部
    };
    
    //重载+运算符
    Complex operator+(const Complex &c1, const Complex &c2) {
    	Complex c;
    	c.m_real = c1.m_real + c2.m_real;
    	c.m_imag = c1.m_imag + c2.m_imag;
    	return c;
    }
    
    int main() {
    	Complex c1(25, 35);
    	Complex c2 = c1 + 15.6;
    	Complex c3 = 28.23 + c1; // Complex类型可以和double类型相加,但并没有对针对这两个类型重载 +
    	cout << c2.real() << " + " << c2.imag() << "i" << endl;
    	cout << c3.real() << " + " << c3.imag() << "i" << endl;
    
    	return 0;
    }
    
  • 全局重载+的原因:保证 + 运算符的操作数能够被对称的处理。

    1. 例如,如果将operator+定义为成员函数,根据“+ 运算符具有左结合性”这条原则,Complex c2 = c1 + 15.6;会被转换为下面的形式:Complex c2 = c1.operator+(Complex(15.6))
    2. 而对于Complex c3 = 28.23 + c1;,编译器会尝试转换为不同的形式:Complex c3 = (28.23).operator+(c1);,而double 类型并没有以成员函数的形式重载 +
    3. C++ 只会对成员函数的参数进行类型转换,而不会对调用成员函数的对象进行类型转换。因此,无法实现把 28.23 先转换成 Complex 类型再相加,Complex c3 = Complex(28.23).operator+(c1);

C++重载>>和<<

#include <iostream>
using namespace std;

class complex {
public:
	complex(double real = 0.0, double imag = 0.0) : m_real(real), m_imag(imag) { };
public:
	friend complex operator+(const complex & A, const complex & B);
	friend complex operator-(const complex & A, const complex & B);
	friend complex operator*(const complex & A, const complex & B);
	friend complex operator/(const complex & A, const complex & B);
	friend istream & operator>>(istream & in, complex & A);
	friend ostream & operator<<(ostream & out, complex & A);
private:
	double m_real;  //实部
	double m_imag;  //虚部
};

//重载加法运算符
complex operator+(const complex & A, const complex &B) {
	complex C;
	C.m_real = A.m_real + B.m_real;
	C.m_imag = A.m_imag + B.m_imag;
	return C;
}

//重载减法运算符
complex operator-(const complex & A, const complex &B) {
	complex C;
	C.m_real = A.m_real - B.m_real;
	C.m_imag = A.m_imag - B.m_imag;
	return C;
}

//重载乘法运算符
complex operator*(const complex & A, const complex &B) {
	complex C;
	C.m_real = A.m_real * B.m_real - A.m_imag * B.m_imag;
	C.m_imag = A.m_imag * B.m_real + A.m_real * B.m_imag;
	return C;
}

//重载除法运算符
complex operator/(const complex & A, const complex & B) {
	complex C;
	double square = A.m_real * A.m_real + A.m_imag * A.m_imag;
	C.m_real = (A.m_real * B.m_real + A.m_imag * B.m_imag) / square;
	C.m_imag = (A.m_imag * B.m_real - A.m_real * B.m_imag) / square;
	return C;
}

//重载输入运算符
istream & operator>>(istream & in, complex & A) {
	in >> A.m_real >> A.m_imag;
	return in;
}

//重载输出运算符
ostream & operator<<(ostream & out, complex & A) {
	out << A.m_real << " + " << A.m_imag << " i ";;
	return out;
}

int main() {
	complex c1, c2, c3;
	cin >> c1 >> c2;

	c3 = c1 + c2;
	cout << "c1 + c2 = " << c3 << endl;

	c3 = c1 - c2;
	cout << "c1 - c2 = " << c3 << endl;

	c3 = c1 * c2;
	cout << "c1 * c2 = " << c3 << endl;

	c3 = c1 / c2;
	cout << "c1 / c2 = " << c3 << endl;

	return 0;
}

C++重载[](下标运算符)

#include <iostream>
using namespace std;

class Array{
public:
    Array(int length = 0);
    ~Array();
public:
    int & operator[](int i);
    const int & operator[](int i) const;
public:
    int length() const { return m_length; }
    void display() const;
private:
    int m_length;  //数组长度
    int *m_p;  //指向数组内存的指针
};

Array::Array(int length): m_length(length){
    if(length == 0){
        m_p = NULL;
    }else{
        m_p = new int[length];
    }
}

Array::~Array(){
    delete[] m_p;
}

int& Array::operator[](int i){
    return m_p[i];
}

const int & Array::operator[](int i) const{
    return m_p[i];
}

void Array::display() const{
    for(int i = 0; i < m_length; i++){
        if(i == m_length - 1){
            cout<<m_p[i]<<endl;
        }else{
            cout<<m_p[i]<<", ";
        }
    }
}

int main(){
    int n;
    cin>>n;

    Array A(n);
    for(int i = 0, len = A.length(); i < len; i++){
        A[i] = i * 5;
    }
    A.display();
   
    const Array B(n);
    cout<<B[n-1]<<endl;  //访问最后一个元素
   
    return 0;
}

C++重载++和--(自增和自减运算符)

函数中参数n是没有任何意义的,它的存在只是为了区分是前置形式还是后置形式。

#include <iostream>
#include <iomanip>
using namespace std;

//秒表类
class stopwatch {
public:
	stopwatch() : m_min(0), m_sec(0) { }
public:
	void setzero() { m_min = 0; m_sec = 0; }
	stopwatch run();  // 运行
	stopwatch operator++();  //++i,前置形式
	stopwatch operator++(int);  //i++,后置形式
	friend ostream & operator<<(ostream &, const stopwatch &);
private:
	int m_min;  //分钟
	int m_sec;  //秒钟
};

stopwatch stopwatch::run() {
	++m_sec;
	if (m_sec == 60) {
		m_min++;
		m_sec = 0;
	}
	return *this;
}

stopwatch stopwatch::operator++() {
	return run();
}

stopwatch stopwatch::operator++(int n) {
	stopwatch s = *this;
	run();
	return s;
}

ostream &operator<<(ostream & out, const stopwatch & s) {
	out << setfill('0') << setw(2) << s.m_min << ":" << setw(2) << s.m_sec;
	return out;
}

int main() {
	stopwatch s1, s2;

	s1 = s2++;
	cout << "s1: " << s1 << endl;
	cout << "s2: " << s2 << endl;
	s1.setzero();
	s2.setzero();

	s1 = ++s2;
	cout << "s1: " << s1 << endl;
	cout << "s2: " << s2 << endl;
	return 0;
}

C++重载new和delete运算符

C++重载()(强制类型转换运算符)

#include <iostream>
using namespace std;
class Complex
{
    double real, imag;
public:
    Complex(double r = 0, double i = 0) :real(r), imag(i) {};
    operator double() { return real; }  //重载强制类型转换运算符 double
};
int main()
{
    Complex c(1.2, 3.4);
    cout << (double)c << endl;  //输出 1.2
    double n = 2 + c;  //等价于 double n = 2 + c. operator double()
    cout << n;  //输出 3.2
}

模板

C++函数模板

#include <iostream>
using namespace std;

//声明函数模板
template<typename T> T max(T a, T b, T c);

int main() {
	//求三个整数的最大值
	int i1, i2, i3, i_max;
	cin >> i1 >> i2 >> i3;
	i_max = max(i1, i2, i3);
	cout << "i_max=" << i_max << endl;

	//求三个浮点数的最大值
	double d1, d2, d3, d_max;
	cin >> d1 >> d2 >> d3;
	d_max = max(d1, d2, d3);
	cout << "d_max=" << d_max << endl;

	//求三个长整型数的最大值
	long g1, g2, g3, g_max;
	cin >> g1 >> g2 >> g3;
	g_max = max(g1, g2, g3);
	cout << "g_max=" << g_max << endl;

	return 0;
}

//定义函数模板
template<typename T>  //模板头,这里不能有分号
T max(T a, T b, T c) { //函数头
	T max_num = a;
	if (b > max_num) max_num = b;
	if (c > max_num) max_num = c;
	return max_num;
}

C++类模板

#include <iostream>
using namespace std;

template<class T1, class T2>  //这里不能有分号
class Point {
public:
	Point(T1 x, T2 y) : m_x(x), m_y(y) { }
public:
	T1 getX() const;  //获取x坐标
	void setX(T1 x);  //设置x坐标
	T2 getY() const;  //获取y坐标
	void setY(T2 y);  //设置y坐标
private:
	T1 m_x;  //x坐标
	T2 m_y;  //y坐标
};

template<class T1, class T2>  //模板头
T1 Point<T1, T2>::getX() const /*函数头*/ {
	return m_x;
}

template<class T1, class T2>
void Point<T1, T2>::setX(T1 x) {
	m_x = x;
}

template<class T1, class T2>
T2 Point<T1, T2>::getY() const {
	return m_y;
}

template<class T1, class T2>
void Point<T1, T2>::setY(T2 y) {
	m_y = y;
}

int main() {
	Point<int, int> p1(10, 20);
	cout << "x=" << p1.getX() << ", y=" << p1.getY() << endl;

	Point<int, const char*> p2(10, "东经180度");
	cout << "x=" << p2.getX() << ", y=" << p2.getY() << endl;

	Point<const char*, const char*> *p3 = new Point<const char*, const char*>("东经180度", "北纬210度");
	cout << "x=" << p3->getX() << ", y=" << p3->getY() << endl;

	return 0;
}

C++函数模板的重载

#include <iostream>
using namespace std;

template<class T> void Swap(T &a, T &b);  //模板①:交换基本类型的值
template<typename T> void Swap(T a[], T b[], int len);  //模板②:交换两个数组

void printArray(int arr[], int len);  //打印数组元素

int main() {
	//交换基本类型的值
	int m = 10, n = 99;
	Swap(m, n);  //匹配模板①
	cout << m << ", " << n << endl;

	//交换两个数组
	int a[5] = { 1, 2, 3, 4, 5 };
	int b[5] = { 10, 20, 30, 40, 50 };
	int len = sizeof(a) / sizeof(int);  //数组长度
	Swap(a, b, len);  //匹配模板②
	printArray(a, len);
	printArray(b, len);

	return 0;
}

template<class T> void Swap(T &a, T &b) {
	T temp = a;
	a = b;
	b = temp;
}

template<typename T> void Swap(T a[], T b[], int len) {
	T temp;
	for (int i = 0; i < len; i++) {
		temp = a[i];
		a[i] = b[i];
		b[i] = temp;
	}
}

void printArray(int arr[], int len) {
	for (int i = 0; i < len; i++) {
		if (i == len - 1) {
			cout << arr[i] << endl;
		}
		else {
			cout << arr[i] << ", ";
		}
	}
}

C++函数模板的实参推断

C++模板的显式具体化

C++ 没有办法限制类型参数的范围,我们可以使用任意一种类型来实例化模板。但是模板中的语句(函数体或者类体)不一定就能适应所有的类型,可能会有个别的类型没有意义,或者会导致语法错误。比如,比大小对于地址和类的比较。

C++模板中的非类型参数

C++ 对模板的支持非常自由,模板中除了可以包含类型参数,还可以包含非类型参数

C++异常

  • 程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:

    1. 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。

    2. 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。

    3. 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。

C++异常机制就是为解决运行时错误而引入的。

C++ try catch入门

  • try-catch 的用法::

    • exceptionType是异常类型,它指明了当前的 catch 可以处理什么类型的异常;

      C++ 规定,异常类型可以是 int、char、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。

      C++ 语言本身以及标准库中的函数抛出的异常,都是 exception 类或其子类的异常。也就是说,抛出异常时,会创建一个 exception 类或其子类的对象。

    • variable是一个变量,用来接收异常信息,这和函数传参的过程类似。

     try
     {
        // 可能抛出异常的语句
    }
    catch(exceptionType variable)
    {
        // 处理异常的语句
    }
    
  • catch 和函数调用的区别:可以将 catch 看做一个没有返回值的函数,当异常发生后 catch 会被调用,并且会接收实参(异常数据)。

    • 真正的函数调用,形参和实参的类型必须要匹配,或者可以自动转换,否则在编译阶段就报错了。
    • 而对于 catch,异常是在运行阶段产生的,它可以是任何类型,没法提前预测,所以不能在编译阶段判断类型是否正确,只能等到程序运行后,真的抛出异常了,再将异常类型和 catch 能处理的类型进行匹配,匹配成功的话就“调用”当前的 catch,否则就忽略当前的 catch。
  • 示例1:

    #include <iostream>
    #include <string>
    #include <exception>
    using namespace std;
    
    int main(){
        string str = "http://c.biancheng.net";
      
        try{
            char ch1 = str[100]; // [ ] 越界不会抛出异常
            cout<<ch1<<endl;
        }catch(exception e){
            cout<<"[1]out of bound!"<<endl;
        }
        
        try{
            char ch2 = str.at(100); // at 越界会抛出异常
            cout<<ch2<<endl;
        }catch(exception &e){  //exception类位于<exception>头文件中
            cout<<"[2]out of bound!"<<endl;
        }
        
        return 0;
    }
    
  • 示例2:

    #include <iostream>
    #include <string>
    #include <exception>
    using namespace std;
    
    void func(){
        throw "Unknown Exception";  //抛出异常
        cout<<"[1]This statement will not be executed."<<endl;
    }
    
    int main(){
        
        try{
            func();
            cout<<"[2]This statement will not be executed."<<endl;
        }catch(const char* &e){
            cout<<e<<endl;
        }
        
        return 0;
    }
    

C++异常类型以及多级catch匹配

  • 多级catch示例:

    #include <iostream>
    #include <string>
    using namespace std;
    
    // 运行结果:Exception type: Base----我们期望的是,异常被catch(Derived)捕获,但是从输出结果可以看出,异常提前被catch(Base)捕获了,这说明 catch 在匹配异常类型时发生了向上转型
    class Base{ };
    
    class Derived: public Base{ };
    
    int main(){
        try{
            throw Derived();  //抛出自己的异常类型,实际上是创建一个Derived类型的匿名对象
            cout<<"This statement will not be executed."<<endl;
        }catch(int){
            cout<<"Exception type: int"<<endl;
        }catch(char *){
            cout<<"Exception type: cahr *"<<endl;
        }catch(Base){  //匹配成功(向上转型)
            cout<<"Exception type: Base"<<endl;
        }catch(Derived){
            cout<<"Exception type: Derived"<<endl;
        }
        return 0;
    }
    

C++ throw(抛出异常)

使用 throw 关键字可以来显式地抛出异常,语法为throw exceptionData,exceptionData 是“异常数据”的意思,可以是 int、float、bool 等基本类型,也可以是指针、数组、字符串、结构体、类等聚合类型。

  • 异常规范(不要使用,仅了解):也称为异常指示符或异常列表,用在函数头和函数体之间的throw 关键字,指明当前函数能够抛出的异常类型。

    • 示例:

      class Base{
      public:
          virtual int fun1(int) throw();
          virtual int fun2(int) throw(int);
          virtual string fun3() throw(int, string);
      };
      
      class Derived:public Base{
      public:
          int fun1(int) throw(int);   //错!异常规范不如 throw() 严格
          int fun2(int) throw(int);   //对!有相同的异常规范
          string fun3() throw(string);  //对!异常规范比 throw(int,string) 更严格
      }
      
  • 动态数组示例,一个典型的使用异常的场景:

    #include <iostream>
    #include <cstdlib>
    using namespace std;
    
    //自定义的异常类型
    class OutOfRange{
    public:
        OutOfRange(): m_flag(1){ };
        OutOfRange(int len, int index): m_len(len), m_index(index), m_flag(2){ }
    public:
        void what() const;  //获取具体的错误信息
    private:
        int m_flag;  //不同的flag表示不同的错误
        int m_len;  //当前数组的长度
        int m_index;  //当前使用的数组下标
    };
    
    void OutOfRange::what() const {
        if(m_flag == 1){
            cout<<"Error: empty array, no elements to pop."<<endl;
        }else if(m_flag == 2){
            cout<<"Error: out of range( array length "<<m_len<<", access index "<<m_index<<" )"<<endl;
        }else{
            cout<<"Unknown exception."<<endl;
        }
    }
    
    //实现动态数组
    class Array{
    public:
        Array();
        ~Array(){ free(m_p); };
    public:
        int operator[](int i) const;  //获取数组元素
        int push(int ele);  //在末尾插入数组元素
        int pop();  //在末尾删除数组元素
        int length() const{ return m_len; };  //获取数组长度
    private:
        int m_len;  //数组长度
        int m_capacity;  //当前的内存能容纳多少个元素
        int *m_p;  //内存指针
    private:
        static const int m_stepSize = 50;  //每次扩容的步长
    };
    
    Array::Array(){
        m_p = (int*)malloc( sizeof(int) * m_stepSize );
        m_capacity = m_stepSize;
        m_len = 0;
    }
    
    // 通过重载过的[ ]运算符来访问数组元素,如果下标过小或过大,就会抛出异常
    int Array::operator[](int index) const {
        if( index<0 || index>=m_len ){  //判断是否越界
            throw OutOfRange(m_len, index);  //抛出异常(创建一个匿名对象)
        }
        return *(m_p + index);
    }
    
    int Array::push(int ele){
        if(m_len >= m_capacity){  //如果容量不足就扩容
            m_capacity += m_stepSize;
            m_p = (int*)realloc( m_p, sizeof(int) * m_capacity );  //扩容
        }
        *(m_p + m_len) = ele;
        m_len++;
        return m_len-1;
    }
    
    // 使用 pop() 删除数组元素时,如果当前数组为空,会抛出错误。
    int Array::pop(){
        if(m_len == 0){
             throw OutOfRange();  //抛出异常(创建一个匿名对象)
        }
        m_len--;
        return *(m_p + m_len);
    }
    
    //打印数组元素
    void printArray(Array &arr){
        int len = arr.length();
        //判断数组是否为空
        if(len == 0){
            cout<<"Empty array! No elements to print."<<endl;
            return;
        }
        for(int i=0; i<len; i++){
            if(i == len-1){
                cout<<arr[i]<<endl;
            }else{
                cout<<arr[i]<<", ";
            }
        }
    }
    
    int main(){
        
        Array nums;
        //向数组中添加十个元素
        for(int i=0; i<10; i++){
            nums.push(i);
        }
        printArray(nums);
        
        //尝试访问第20个元素
        try{
            cout<<nums[20]<<endl;
        }catch(OutOfRange &e){
            e.what();
        }
        
        //尝试弹出20个元素
        try{
            for(int i=0; i<20; i++){
                nums.pop();
            }
        }catch(OutOfRange &e){
            e.what();
        }
        
        printArray(nums);
        return 0;
    }
    

C++ exception类:C++标准异常的基类

面向对象进阶

C++拷贝构造函数(复制构造函数)

拷贝是在初始化阶段进行的,也就是用其它对象的数据来初始化新对象的内存。

  • 对象创建的两个阶段:

    1. 分配内存空间:就是在堆区、栈区或者全局数据区留出足够多的字节,此时内存所包含的数据一般是零值或者随机值,没有实际的意义。
    2. 初始化:就是首次对内存赋值,再次赋值不叫初始化。初始化的时候还可以为对象分配其他的资源(打开文件、连接网络、动态分配内存等),或者提前进行一些计算(根据长度和宽度计算出矩形的面积等)等。即调用构造函数。
  • 拷贝构造使用注意事项:拷贝构造函数的参数必须是当前类的const引用。

    1. 如果拷贝构造函数的参数不是当前类的引用,而是当前类的对象,那么在调用拷贝构造函数时,会将另外一个对象直接传递给形参,这本身就是一次拷贝,会再次调用拷贝构造函数,然后又将一个对象直接传递给了形参,将继续调用拷贝构造函数……这个过程会一直持续下去,没有尽头,陷入死循环。
    2. 拷贝构造函数的目的是用其它对象的数据来初始化当前对象,并没有期望更改其它对象的数据,故添加 const 限制。

初始化和赋值

  • 初始化和赋值的区别:在定义的同时进行赋值叫做初始化,定义完成以后再赋值(不管在定义的时候有没有赋值)就叫做赋值。初始化只能有一次,赋值可以有多次。对于基本类型的数据,很少会区分初始化和赋值这两个概念,即使将它们混淆,也不会出现什么错误。但是对于类,它们的区别就非常重要了,因为初始化时会调用构造函数(以拷贝的方式初始化时会调用拷贝构造函数),而赋值时会调用重载过的赋值运算符。

    • 示例1:

      int a = 100;  //以赋值的方式初始化
      a = 200;  //赋值
      a = 300;  //赋值
      int b;  //默认初始化
      b = 29;  //赋值
      b = 39;  //赋值
      
    • 示例2:

      int main(){
          //stu1、stu2、stu3都会调用普通构造函数Student(string name, int age, float score)
          Student stu1("小明", 16, 90.5);
          Student stu2("王城", 17, 89.0);
          Student stu3("陈晗", 18, 98.0);
         
          Student stu4 = stu1;  //调用拷贝构造函数Student(const Student &stu)
          stu4 = stu2;  //赋值,调用operator=()
          stu4 = stu3;  //赋值,调用operator=()
         
          Student stu5;  //调用普通构造函数Student()
          stu5 = stu1;  //赋值,调用operator=()
          stu5 = stu2;  //赋值,调用operator=()
         
          return 0;
      }
      

C++深拷贝和浅拷贝

如果一个类拥有指针类型的成员变量,那么绝大部分情况下就需要深拷贝,因为只有这样,才能将指针指向的内容再复制出一份来,让原有对象和新生对象相互独立,彼此之间不受影响。

  • 浅拷贝:基本数据类型与类对象,若以拷贝的方式进行初始化,就是将一块内存中的数据按照二进制位(Bit)复制到当前对象所在的内存,这种默认的拷贝行为就是浅拷贝,这和调用 memcpy() 函数的效果非常类似。

    • 示例:

      class Base{
      public:
          Base(): m_a(0), m_b(0){ }
          Base(int a, int b): m_a(a), m_b(b){ }
      private:
          int m_a;
          int m_b;
      };
      int main(){
          int a = 10;
          int b = a;  //拷贝
          Base obj1(10, 20);
          Base obj2 = obj1;  //拷贝
          return 0;
      }
      
  • 深拷贝:将对象所持有的其它资源一并拷贝的行为叫做深拷贝,例如动态分配的内存、指向其他数据的指针等。

    • 示例:
    Array::Array(const Array &arr){  //拷贝构造函数
        this->m_len = arr.m_len;
        this->m_p = (int*)calloc( this->m_len, sizeof(int) );
        memcpy( this->m_p, arr.m_p, m_len * sizeof(int) );
    }
    Array::~Array(){ free(m_p); }
    

C++重载=(赋值运算符)

对于简单的类,默认的赋值运算符一般就够用了,我们也没有必要再显式地重载它。但是当类持有其它资源时,例如动态分配的内存、打开的文件、指向其他数据的指针、网络连接等,默认的赋值运算符就不能处理了,我们必须显式地重载它,这样才能将原有对象的所有数据都赋值给新对象。

C++拷贝控制操作(三/五法则)

  • C++三法则:拷贝控制操作是由三个特殊的成员函数来完成的,,分别是拷贝构造函数、赋值运算符和析构函数。

  • C++五法则:为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数。

  • 拷贝控制操作的两个原则:

    1. 如果一个类需要定义析构函数,那么几乎可以肯定它也需要定义拷贝构造函数和赋值运算符。
    2. 如果一个类需要一个拷贝构造函数,几乎可以肯定它也需要一个赋值运算符;反之亦然。然而,无论需要拷贝构造函数还是需要赋值运算符,都不必然意味着也需要析构函数。

C++转换构造函数:将其它类型转换为当前类的类型

将其它类型转换为当前类类型需要借助转换构造函数。转换构造函数只有一个参数。

  • 示例:

    #include <iostream>
    using namespace std;
    
    //复数类
    class Complex{
    public:
        Complex(): m_real(0.0), m_imag(0.0){ }
        Complex(double real, double imag): m_real(real), m_imag(imag){ }
        Complex(double real): m_real(real), m_imag(0.0){ }  //转换构造函数
    public:
        friend ostream & operator<<(ostream &out, Complex &c);  //友元函数
    private:
        double m_real;  //实部
        double m_imag;  //虚部
    };
    
    //重载>>运算符
    ostream & operator<<(ostream &out, Complex &c){
        out << c.m_real <<" + "<< c.m_imag <<"i";;
        return out;
    }
    
    int main(){
        Complex a(10.0, 20.0);
        cout<<a<<endl;
        a = 25.5;  //调用转换构造函数
        cout<<a<<endl;
        return 0;
    }
    

C++类型转换函数:将当前类的类型转换为其它类型

  • 示例:

    #include <iostream>
    using namespace std;
    
    //复数类
    class Complex{
    public:
        Complex(): m_real(0.0), m_imag(0.0){ }
        Complex(double real, double imag): m_real(real), m_imag(imag){ }
    public:
        friend ostream & operator<<(ostream &out, Complex &c);
        friend Complex operator+(const Complex &c1, const Complex &c2);
        operator double() const { return m_real; }  //类型转换函数
    private:
        double m_real;  //实部
        double m_imag;  //虚部
    };
    
    //重载>>运算符
    ostream & operator<<(ostream &out, Complex &c){
        out << c.m_real <<" + "<< c.m_imag <<"i";;
        return out;
    }
    
    //重载+运算符
    Complex operator+(const Complex &c1, const Complex &c2){
        Complex c;
        c.m_real = c1.m_real + c2.m_real;
        c.m_imag = c1.m_imag + c2.m_imag;
        return c;
    }
    
    int main(){
        Complex c1(24.6, 100);
        double f = c1;  //相当于 double f = Complex::operator double(&c1);
        cout<<"f = "<<f<<endl;
        f = 12.5 + c1 + 6;  //相当于 f = 12.5 + Complex::operator double(&c1) + 6;
        cout<<"f = "<<f<<endl;
        int n = Complex(43.2, 9.3);  //先转换为 double,再转换为 int
        cout<<"n = "<<n<<endl;
        return 0;
    }
    

C++转换构造函数和类型转换函数(进阶)

C/C++类型转换的本质

C++ static_cast、dynamic_cast、const_cast和reinterpret_cast(四种类型转换运算符)

  • C++四种类型转换运算符:

    关键字 说明
    static_cast 用于良性转换,一般不会导致意外发生,风险很低。
    const_cast 用于 const 与非 const、volatile 与非 volatile 之间的转换。
    reinterpret_cast 高度危险的转换,这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,但是可以实现最灵活的 C++ 类型转换。
    dynamic_cast 借助 RTTI,用于类型安全的向下转型(Downcasting)。
  • 示例1:

    // 老式的C风格
    double scores = 95.5;
    int n = (int)scores;
    
    //C++新风格
    double scores = 95.5;
    int n = static_cast<int>(scores);
    

输入输出流

C++输入流和输出流

  • C语言数据读写(I/O)的解决方案:

    1. 使用 scanf()、gets() 等函数从键盘读取数据,使用 printf()、puts() 等函数向屏幕上输出数据;
    2. 使用 fscanf()、fgets() 等函数读取文件中的数据,使用 fprintf()、fputs() 等函数向文件中写入数据。
  • C++各流类各自的功能分别为:

    • istream:常用于接收从键盘输入的数据;
    • ostream:常用于将数据输出到屏幕上;
    • ifstream:用于读取文件中的数据;
    • ofstream:用于向文件中写入数据;
    • iostream:继承自 istream 和 ostream 类,因为该类的功能兼两者于一身,既能用于输入,也能用于输出;
    • fstream:兼 ifstream 和 ofstream 类功能于一身,既能读取文件中的数据,又能向文件中写入数据。
  • cout、cerr 和 clog 之间的区别如下:

    cerr 常用来输出警告和错误信息给程序的使用者,clog 常用来输出程序执行过程中的日志信息(此部分信息只有程序开发者看得到,不需要对普通用户公开)。

    1. cout 除了可以将数据输出到屏幕上,通过重定向(后续会讲),还可以实现将数据输出到指定文件中;而 cerr 和 clog 都不支持重定向,它们只能将数据输出到屏幕上;
    2. cout 和 clog 都设有缓冲区,即它们在输出数据时,会先将要数据放到缓冲区,等缓冲区满或者手动换行(使用换行符 '\n' 或者 endl)时,才会将数据全部显示到屏幕上;而 cerr 则不设缓冲区,它会直接将数据输出到屏幕上。
  • cin 输入流对象常用成员方法:

    成员方法名 功能
    getline(str,n,ch) 从输入流中接收 n-1 个字符给 str 变量,当遇到指定 ch 字符时会停止读取,默认情况下 ch 为 '\0'。
    get() 从输入流中读取一个字符,同时该字符会从输入流中消失。
    gcount() 返回上次从输入流提取出的字符个数,该函数常和 get()、getline()、ignore()、peek()、read()、readsome()、putback() 和 unget() 联用。
    peek() 返回输入流中的第一个字符,但并不是提取该字符。
    putback(c) 将字符 c 置入输入流(缓冲区)。
    ignore(n,ch) 从输入流中逐个提取字符,但提取出的字符被忽略,不被使用,直至提取出 n 个字符,或者当前读取的字符为 ch。
    operator>> 重载 >> 运算符,用于读取指定类型的数据,并返回输入流对象本身。
  • cout 输出流对象常用成员方法:

    成员方法名 功能
    put() 输出单个字符。
    write() 输出指定的字符串。
    tellp() 用于获取当前输出流指针的位置。
    seekp() 设置输出流指针的位置。
    flush() 刷新输出流缓冲区。
    operator<< 重载 << 运算符,使其用于输出其后指定类型的数据。
  • 示例:

    #include <iostream>
    using namespace std;
    
    int main() {
        char url[30] = {0};
        //读取一行字符串
        cin.getline(url, 30);
        //输出上一条语句读取字符串的个数
        cout << "读取了 "<<cin.gcount()<<" 个字符" << endl;
        //输出 url 数组存储的字符串
        cout.write(url, 30);
        return 0;
    }
    

C++ cout.put():输出单个字符

  • 【实例1】输出单个字符 a:cout.put('a');

    • 结果:在屏幕上显示一个字符 a。
  • 【实例2】put() 函数的参数可以是字符或字符的 ASCII 代码(也可以是一个整型表达式)。

    • 结果:上面两行代码都输出字符 a,因为 97 是字符 a 的 ASCII 代码。
cout.put(65 + 32);
cout.put(97);
  • 【实例3】在一个语句中连续调用 put() 函数:cout.put(71).put(79).put(79). put(68).put('\n');
    • 结果:在屏幕上显示GOOD。

C++ cout.write():输出字符串

  • 【例 1】输出 "http://c.biancheng.net/cplus/" 字符串中前 4 个字符。

    #include <iostream>
    using namespace std;
    int main() 
    {    
        const char * str = "http://c.biancheng.net/cplus/";   
        cout.write(str, 4);   
        return 0;
    }
    
    • 结果:http
  • 【例 2】连续使用 write() 方法:

    #include <iostream>
    using namespace std;
    
    int main()
    {    
        cout.write("http://", 7).write("c.biancheng.net", 15).write("/cplus/", 7);    
        return 0;
    }
    
    • 结果:http://c.biancheng.net/cplus/.

C++ cout.tellp()和cout.seekp()方法

当数据暂存于输出流缓冲区中时,仍可以对其进行修改。ostream 类中提供有 tellp() 和 seekp() 成员方法,借助它们就可以修改位于输出流缓冲区中的数据。

C++ cout格式化输出

  • ostream 类的成员方法 :

    成员函数 说明
    flags(fmtfl) 当前格式状态全部替换为 fmtfl。注意,fmtfl 可以表示一种格式,也可以表示多种格式。
    precision(n) 设置输出浮点数的精度为 n。
    width(w) 指定输出宽度为 w 个字符。
    fill(c) 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)。
    setf(fmtfl, mask) 在当前格式的基础上,追加 fmtfl 格式,并删除 mask 格式。其中,mask 参数可以省略。
    unsetf(mask) 在当前格式的基础上,删除 mask 格式。
  • fmtfl 和 mask 参数可选值:

    标 志 作 用
    ios::boolapha 把 true 和 false 输出为字符串
    ios::left 输出数据在本域宽范围内向左对齐
    ios::right 输出数据在本域宽范围内向右对齐
    ios::internal 数值的符号位在域宽内左对齐,数值右对齐,中间由填充字符填充
    ios::dec 设置整数的基数为 10
    ios::oct 设置整数的基数为 8
    ios::hex 设置整数的基数为 16
    ios::showbase 强制输出整数的基数(八进制数以 0 开头,十六进制数以 0x 打头)
    ios::showpoint 强制输出浮点数的小点和尾数 0
    ios::uppercase 在以科学记数法格式 E 和以十六进制输出字母时以大写表示
    ios::showpos 对正数显示“+”号
    ios::scientific 浮点数以科学记数法格式输出
    ios::fixed 浮点数以定点格式(小数形式)输出
    ios::unitbuf 每次输出之后刷新所有的流
  • 示例1:

    #include <iostream>
    using namespace std;
    
    int main()
    {
        double a = 1.23;
        //设定后续输出的浮点数的精度为 4
        cout.precision(4);
        cout <<"precision: "<< a << endl;
        //设定后续以科学计数法的方式输出浮点数
        cout.setf(ios::scientific);
        cout <<"scientific:"<< a << endl;
        return 0;
    }
    
    • 结果:

      precision: 1.23
      scientific:1.2300e+00
      
  • C++ 流操纵算子:

流操纵算子 作 用
*dec 以十进制形式输出整数
hex 以十六进制形式输出整数
oct 以八进制形式输出整数
fixed 以普通小数形式输出浮点数
scientific 以科学计数法形式输出浮点数
left 左对齐,即在宽度不足时将填充字符添加到右边
*right 右对齐,即在宽度不足时将填充字符添加到左边
setbase(b) 设置输出整数时的进制,b=8、10 或 16
setw(w) 指定输出宽度为 w 个字符,或输入字符串时读入 w 个字符。注意,该函数所起的作用是一次性的,即只影响下一次 cout 输出。
setfill(c) 在指定输出宽度的情况下,输出的宽度不足时用字符 c 填充(默认情况是用空格填充)
setprecision(n) 设置输出浮点数的精度为 n。 在使用非 fixed 且非 scientific 方式输出的情况下,n 即为有效数字最多的位数,如果有效数字位数超过 n,则小数部分四舍五人,或自动变为科学计 数法输出并保留一共 n 位有效数字。 在使用 fixed 方式和 scientific 方式输出的情况下,n 是小数点后面应保留的位数。
setiosflags(mask) 在当前格式状态下,追加 mask 格式,mask 参数可选择表 2 中的所有值。
resetiosflags(mask) 在当前格式状态下,删除 mask 格式,mask 参数可选择表 2 中的所有值。
boolapha 把 true 和 false 输出为字符串
  • 示例2:

    #include <iostream>
    #include <iomanip>
    using namespace std;
    
    int main()
    {
        //以十六进制输出整数
        cout << hex << 16 << endl;
        //删除之前设定的进制格式,以默认的 10 进制输出整数
        cout << resetiosflags(ios::basefield)<< 16 << endl;
        double a = 123;
        //以科学计数法的方式输出浮点数
        cout << scientific << a << endl;
        //删除之前设定的科学计数法的方法
        cout << resetiosflags(ios::scientific) << a << endl;
        return 0;
    }
    
    • 结果:

      10
      16
      1.230000e+02
      123
      

C++对输入输出重定向

在默认情况下,cin 只能接收从键盘输入的数据,cout 也只能将数据输出到屏幕上。但通过重定向,cin 可以将指定文件作为输入源,即接收文件中早已准备好的数据,同样 cout 可以将原本要输出到屏幕上的数据转而写到指定文件中。

  • C++实现重定向的3种常用方式:

    1. freopen()函数实现重定向: 执行此程序之前,我们需要找到当前程序文件所在的目录,并手动创建一个txt 文件

      freopen() 定义在<stdio.h>头文件中,是 C 语言标准库中的函数,专门用于重定向输入流,该函数也可以对 C++ 中的 cin 和 cout 进行重定向。

      • 示例:
      #include <iostream>    //cin、cout
      #include <string>      //string
      #include <stdio.h>     //freopen
      using namespace std;
      int main()  
      {
          string name, url;
          //将标准输入流重定向到 in.txt 文件
          freopen("in.txt", "r", stdin);
          cin >> name >> url;
          //将标准输出重定向到 out.txt文件
          freopen("out.txt", "w", stdout); 
          cout << name << "\n" << url;
          return 0;
      }
      
    2. rdbuf()函数实现重定向:rdbuf() 函数定义在<ios>头文件中,专门用于实现 C++ 输入输出流的重定向。

    3. 通过控制台实现重定向

C++管理输出缓冲区

C++跳过(忽略)指定字符

  • 示例:

    #include <iostream>
    using namespace std;
    int main()
    {
        int n;
        cin.ignore(5, 'A');
        cin >> n;
        cout << n;
        return 0;
    }
    

C++ cin判断输入结束(读取结束)

  • 示例:

    #include <iostream>
    using namespace std;
    int main()
    {
        int n;
        int maxN = 0;
        while (cin >> n){  //输入没有结束,cin 就返回 true,条件就为真
            if (maxN < n)
                maxN = n;
        }
        cout << maxN <<endl;
        return 0;
    }
    

C++处理输入输出错误

文件操作

C++文件类(文件流类)及用法

头文件中定义有 ostream 和 istream 类的对象 cin 和 cout 不同, 头文件中并没有定义可直接使用的 fstream、ifstream 和 ofstream 类对象。因此,如果我们想使用该类操作文件,需要自己创建相应类的对象。

  • 3 个类文件流类:C++ 标准库中提供了 3 个类用于实现文件操作,它们统称为文件流类
    • ifstream:专用于从文件中读取数据;
    • ofstream:专用于向文件中写入数据;
    • fstream:既可用于从文件中读取数据,又可用于向文件中写入数据。
image-20230726100452127
  • 示例:

    #include <iostream>
    #include <fstream>
    using namespace std;
    
    int main() {
        const char *url ="http://c.biancheng.net/cplus/";
        //创建一个 fstream 类对象
        fstream fs;
        //将 test.txt 文件和 fs 文件流关联
        fs.open("test.txt", ios::out);
        //向test.txt文件中写入 url 字符串
        fs.write(url, 30);
        fs.close();
        return 0;
    }
    
  • fstream类常用成员方法:

    成员方法名 适用类对象 功 能
    open() fstream ifstream ofstream 打开指定文件,使其与文件流对象相关联。
    is_open() 检查指定文件是否已打开。
    close() 关闭文件,切断和文件流对象的关联。
    swap() 交换 2 个文件流对象。
    operator>> fstream ifstream 重载 >> 运算符,用于从指定文件中读取数据。
    gcount() 返回上次从文件流提取出的字符个数。该函数常和 get()、getline()、ignore()、peek()、read()、readsome()、putback() 和 unget() 联用。
    get() 从文件流中读取一个字符,同时该字符会从输入流中消失。
    getline(str,n,ch) 从文件流中接收 n-1 个字符给 str 变量,当遇到指定 ch 字符时会停止读取,默认情况下 ch 为 '\0'。
    ignore(n,ch) 从文件流中逐个提取字符,但提取出的字符被忽略,不被使用,直至提取出 n 个字符,或者当前读取的字符为 ch。
    peek() 返回文件流中的第一个字符,但并不是提取该字符。
    putback(c) 将字符 c 置入文件流(缓冲区)。
    operator<< fstream ofstream 重载 << 运算符,用于向文件中写入指定数据。
    put() 向指定文件流中写入单个字符。
    write() 向指定文件中写入字符串。
    tellp() 用于获取当前文件输出流指针的位置。
    seekp() 设置输出文件输出流指针的位置。
    flush() 刷新文件输出流缓冲区。
    good() fstream ofstream ifstream 操作成功,没有发生任何错误。
    eof() 到达输入末尾或文件尾。

C++ open 打开文件

  • 文件打开模式标记:

    模式标记 适用对象 作用
    ios::in ifstream fstream 打开文件用于读取数据。如果文件不存在,则打开出错。
    ios::out ofstream fstream 打开文件用于写入数据。如果文件不存在,则新建该文件;如果文件原来就存在,则打开时清除原来的内容。
    ios::app ofstream fstream 打开文件,用于在其尾部添加数据。如果文件不存在,则新建该文件。
    ios::ate ifstream 打开一个已有的文件,并将文件读指针指向文件末尾(读写指 的概念后面解释)。如果文件不存在,则打开出错。
    ios:: trunc ofstream 打开文件时会清空内部存储的所有数据,单独使用时与 ios::out 相同。
    ios::binary ifstream ofstream fstream 以二进制方式打开文件。若不指定此模式,则以文本模式打开。
    ios::in | ios::out fstream 打开已存在的文件,既可读取其内容,也可向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
    ios::in | ios::out ofstream 打开已存在的文件,可以向其写入数据。文件刚打开时,原有内容保持不变。如果文件不存在,则打开出错。
    ios::in | ios::out | ios::trunc fstream 打开文件,既可读取其内容,也可向其写入数据。如果文件本来就存在,则打开时清除原来的内容;如果文件不存在,则新建该文件。
  • 流对象上执行 open 打开文件:

    #include <iostream>
    #include <fstream>
    using namespace std;
    int main()
    {
        ifstream inFile;
        inFile.open("c:\\tmp\\test.txt", ios::in);
        if (inFile)  //条件成立,则说明文件打开成功
            inFile.close();
        else
            cout << "test.txt doesn't exist" << endl;
        ofstream oFile;
        oFile.open("test1.txt", ios::out);
        if (!oFile)  //条件成立,则说明文件打开出错
            cout << "error 1" << endl;
        else
            oFile.close();
        oFile.open("tmp\\test2.txt", ios::out | ios::in);
        if (oFile)  //条件成立,则说明文件打开成功
            oFile.close();
        else
            cout << "error 2" << endl;
        fstream ioFile;
        ioFile.open("..\\test3.txt", ios::out | ios::in | ios::trunc);
        if (!ioFile)
            cout << "error 3" << endl;
        else
            ioFile.close();
        return 0;
    }
    
  • 使用流类的构造函数打开文件:

    #include <iostream>
    #include <fstream>
    using namespace std;
    int main()
    {
        ifstream inFile("c:\\tmp\\test.txt", ios::in);
        if (inFile)
            inFile.close();
        else
            cout << "test.txt doesn't exist" << endl;
        ofstream oFile("test1.txt", ios::out);
        if (!oFile)
            cout << "error 1";
        else
            oFile.close();
        fstream oFile2("tmp\\test2.txt", ios::out | ios::in);
        if (!oFile2)
            cout << "error 2";
        else
            oFile.close();
        return 0;
    }
    

文本打开方式和二进制打开方式的区别

总的来说,Linux 平台使用哪种打开方式都行;

Windows 平台上最好用 "ios::in | ios::out" 等打开文本文件,用 "ios::binary" 打开二进制文件。但无论哪种平台,用二进制方式打开文件总是最保险的。

  • 文本方式和二进制方式并没有本质上的区别,只是对于换行符的处理不同
    • Linux平台:用文本方式或二进制方式打开文件没有任何区别,因为文本文件以 \n 作为换行符号。
    • Windows平台:在 Windows 平台上,文本文件以连在一起的 \r\n 作为换行符号。
      • 以文本方式打开文件,当读取文件时,程序会将文件中所有的 \r\n 转换成一个字符 \n,即程序会丢弃前面的 \r,只读入 \n。当写入文件时,程序会将 \n 转换成 \r\n 写入。

C++ close()关闭文件方法

调用 open() 方法打开文件,是文件流对象和文件之间建立关联的过程。

调用 close() 方法关闭已打开的文件,就可以理解为是切断文件流对象和文件之间的关联。

注意,close() 方法的功能仅是切断文件流与文件之间的关联,该文件流并会被销毁,其后续还可用于关联其它的文件。

  • 示例:

    #include <iostream>     //std::cout
    #include <fstream>      //std::ofstream
    using namespace std;
    int main()
    {
        const char * url = "http://c.biancheng.net/cplus/";
        //以文本模式打开out.txt
        ofstream destFile("out.txt", ios::out);
        if (!destFile) {
            cout << "文件打开失败" << endl;
            return 0;
        }
        //向out.txt文件中写入 url 字符串
        destFile << url;
        // flush()刷新缓冲区。刷新输出流缓冲区
        destFile.flush();
        //程序抛出一个异常。此时,未close文件,字符串不会被写入。
        // 19和21行如果更换位置,则会被成功写入
        throw "Exception";
        //关闭打开的 out.txt 文件
        destFile.close();
        return 0;
    }
    
  • 总结:C++ 中使用 open() 打开的文件,在读写操作执行完毕后,应及时调用 close() 方法关闭文件,或者对文件执行写操作后及时调用 flush() 方法刷新输出流缓冲区。否则,程序崩溃且没有close()时,字符串会写入失败。

C++文本文件读写操作

  • 文件的读/写操作可分为 2 类:

    1. 文本形式读写文件:使用 >><< 读写文件;
    2. 二进制形式读写文件:使用 read() 和 write() 成员方法读写文件;
  • C++ >><<读写文本文件:

    • >> 输入流运算符:fstream 或者 ifstream 类对象, ios::in 打开模式
    • << 输出流运算符:fstream 或者 ofstream 类对象,ios::out 打开模式
  • 示例:

    #include <iostream>
    #include <fstream>
    using namespace std;
    int main()
    {
        int x,sum=0;
        ifstream srcFile("in.txt", ios::in); //以文本模式打开in.txt备读
        if (!srcFile) { //打开失败
            cout << "error opening source file." << endl;
            return 0;
        }
        ofstream destFile("out.txt", ios::out); //以文本模式打开out.txt备写
        if (!destFile) {
            srcFile.close(); //程序结束前不能忘记关闭以前打开过的文件
            cout << "error opening destination file." << endl;
            return 0;
        }
        //可以像用cin那样用ifstream对象
        while (srcFile >> x) {
            sum += x;
            //可以像 cout 那样使用 ofstream 对象
            destFile << x << " ";
        }
        cout << "sum:" << sum << endl;
        destFile.close();
        srcFile.close();
        return 0;
    }
    

C++ read()和write()读写二进制文件

以二进制存储文件能够减小文件大小。

  • write()写入文件:

    #include <iostream>
    #include <fstream>
    using namespace std;
    class CStudent
    {
    public:
        char szName[20];
        int age;
    };
    int main()
    {
        CStudent s;
        ofstream outFile("students.dat", ios::out | ios::binary);
        while (cin >> s.szName >> s.age)
            outFile.write((char*)&s, sizeof(s));
        outFile.close();
        return 0;
    }
    
  • read()读文件:

    #include <iostream>
    #include <fstream>
    using namespace std;
    class CStudent
    {
    public:
        char szName[20];
        int age;
    };
    int main()
    {
        CStudent s;       
        ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开
        if(!inFile) {
            cout << "error" <<endl;
            return 0;
        }
        while(inFile.read((char *)&s, sizeof(s))) { //一直读到文件结束
            cout << s.szName << " " << s.age << endl;   
        }
        inFile.close();
        return 0;
    }
    

C++ get()和put()读写文件

get() 和 put() 成员方法用于逐个读取文件中存储的字符,或者逐个将字符存储到文件中。

  • put()成员方法:

    #include <iostream>
    #include <fstream>
    using namespace std;
    int main()
    {
        char c;
        //以二进制形式打开文件
        ofstream outFile("out.txt", ios::out | ios::binary);
        if (!outFile) {
            cout << "error" << endl;
            return 0;
        }
        while (cin >> c) {
            //将字符 c 写入 out.txt 文件
            outFile.put(c);
        }
        outFile.close();
        return 0;
    }
    
  • get()成员方法:

    #include <iostream>
    #include <fstream>
    using namespace std;
    int main()
    {
        char c;
        //以二进制形式打开文件
        ifstream inFile("out.txt", ios::out | ios::binary);
        if (!inFile) {
            cout << "error" << endl;
            return 0;
        }
        while ( (c=inFile.get())&&c!=EOF )   //或者 while(inFile.get(c)),对应第二种语法格式
        {
            cout << c ;
        }
        inFile.close();
        return 0;
    }
    

C++ getline():从文件中读取一行字符串

  • 读取一行数据:

    #include <iostream>
    #include <fstream>
    using namespace std;
    int main()
    {
        char c[40];
        //以二进制模式打开 in.txt 文件
        ifstream inFile("in.txt", ios::in | ios::binary);
        //判断文件是否正常打开
        if (!inFile) {
            cout << "error" << endl;
            return 0;
        }
        //从 in.txt 文件中读取一行字符串,最多不超过 39 个
        // inFile.getline(c,40,'c'); 一旦遇到字符 'c',getline() 方法就会停止读取。
        inFile.getline(c, 40);
        cout << c ;
        inFile.close();
        return 0;
    }
    
  • 读取多行数据:

    #include <iostream>
    #include <fstream>
    using namespace std;
    int main()
    {
        char c[40];
        ifstream inFile("in.txt", ios::in | ios::binary);
        if (!inFile) {
            cout << "error" << endl;
            return 0;
        }
        //连续以行为单位,读取 in.txt 文件中的数据
        while (inFile.getline(c, 40)) {
            cout << c << endl;
        }
        inFile.close();
        return 0;
    }
    

C++移动和获取文件读写指针(seekp、seekg、tellg、tellp)

  • 文件的读写指针:

    • 作用:若要直接跳到文件中的某处开始读写,则需要先将文件的读写指针指向该处,然后再进行读写。
    • 文件读指针:ifstream 类和 fstream 类有 seekg 成员函数,可以设置文件读指针的位置。
    • 文件写指针:ofstream 类和 fstream 类有 seekp 成员函数,可以设置文件写指针的位置。
  • 示例:

    #include <iostream>
    #include <fstream>
    #include <cstring>
    using namespace std;
    class CStudent
    {
        public:
            char szName[20];
            int age;
    };
    int main()
    {
        CStudent s;       
        fstream ioFile("students.dat", ios::in|ios::out);//用既读又写的方式打开
        if(!ioFile) {
            cout << "error" ;
            return 0;
        }
        ioFile.seekg(0,ios::end); //定位读指针到文件尾部,
                                  //以便用以后tellg 获取文件长度
        int L = 0,R; // L是折半查找范围内第一个记录的序号
                      // R是折半查找范围内最后一个记录的序号
        R = ioFile.tellg() / sizeof(CStudent) - 1;
        //首次查找范围的最后一个记录的序号就是: 记录总数- 1
        do {
            int mid = (L + R)/2; //要用查找范围正中的记录和待查找的名字比对
            ioFile.seekg(mid *sizeof(CStudent),ios::beg); //定位到正中的记录
            ioFile.read((char *)&s, sizeof(s));
            int tmp = strcmp( s.szName,"Jack");
            if(tmp == 0) { //找到了
                s.age = 20;
                ioFile.seekp(mid*sizeof(CStudent),ios::beg);
                ioFile.write((char*)&s, sizeof(s));
                break;
            }
            else if (tmp > 0) //继续到前一半查找
                R = mid - 1 ;
            else  //继续到后一半查找
                L = mid + 1;
        }while(L <= R);
        ioFile.close();
        return 0;
    }
    

C++多文件编程

C++多文件编程

  • 一个完整的 C++ 项目可由两类文件组成:
    1. .h 文件:又称“头文件”,用于存放常量、函数的声明部分、类的声明部分;
    2. .cpp 文件:又称“源文件”,用于存放变量、函数的定义部分,类的实现部分。

C++防止头文件被重复引入

  • 方法1:使用宏定义避免重复引入,#ifndef / #define / #endif
  • 方法2: 使用#pragma once避免重复引入
  • 方法3: 使用_Pragma操作符,将如下语句添加到相应文件的开头,_Pragma("once")

C++命名空间应用在多文件编程中

C++ 引入命名空间是为了避免合作开发项目时产生命名冲突,当进行多文件编程时,命名空间常位于 .h 头文件中。

  • 可在不同的头文件中使用不同的命名空间。

  • 可在不同头文件中使用名称相同的命名空间,但前提是位于该命名空间中的成员必须保证互不相同。

  • 示例:

    //demo1.h
    #ifndef _DEMO1_H
    #define _DEMO1_H
    #include<iostream>
    namespace demo {
        void display() {
            std::cout << "demo1::display" << std::endl;
        }
        int num=20;
    }
    #endif
    
    //demo2.h
    #ifndef _DEMO2_H
    #define _DEMO2_H
    #include <iostream>
    namespace demo {
        void display(int a) {
            std::cout << "demo2::display" << std::endl;
        }
        //int num; 因为 demo1.h 中已经声明有同名的变量,取消注释会造成重定义错误
    }
    #endif
    
    //main.cpp
    #include <iostream>
    #include "demo1.h"
    #include "demo2.h"
    int main() {
        // 重载
        demo::display();
        demo::display(2);
        std::cout << demo::num << std::endl;
        return 0;
    }
    

C++ const常量在多文件编程中使用

C++ 中 const 关键字的功能有 2 个,除了表明其修饰的变量为常量外,还将所修饰变量的可见范围限制为当前文件。

用 const 修饰的变量必须在定义的同时进行初始化操作,除非用 extern 修饰。

  • C++ 多文件编程中定义 const 常量的方法:

    1. 将const常量定义在.h头文件中

      //demo.h
      #ifndef _DEMO_H
      #define _DEMO_H
      const int num = 10;
      #endif
      
      //main.cpp
      #include <iostream>
      #include"demo.h"
      int main() {
          std::cout << num << std::endl;
          return 0;
      }
      
    2. 借助extern先声明再定义const常量

      //demo.h
      #ifndef _DEMO_H
      #define _DEMO_H
      extern const int num;  //声明 const 常量
      #endif
      
      //demo.cpp
      #include "demo.h"   //一定要引入该头文件
      const int num =10;  //定义 .h 文件中声明的 num 常量
      
      //main.cpp
      #include <iostream>
      #include "demo.h"
      int main() {
          std::cout << num << std::endl;
          return 0;
      }
      
    3. 借助extern直接定义const常量

      //demo.cpp
      extern const int num =10;
      
      //main.cpp
      #include <iostream>
      extern const int num;
      int main() {
          std::cout << num << std::endl;
          return 0;
      }
      
posted on 2024-01-22 10:47  WilliamMoa  阅读(59)  评论(0)    收藏  举报