C++基础入门笔记

C++基础入门笔记

目录:

  • 概述
  • 第一章 C与C++
  • 第二章 类和对象
  • 第三章 操作符重载
  • 第四章 继承与多态
  • 第五章 指针与引用
  • 第六章 异常和I/O流

概述

一、C++语言语法基础
1.从C到C++的过渡
2.类和对象
3.操作符重载
4.继承与多态
5.异常和I/O流

二、数据结构和算法
1.基本数据结构,堆栈、队列、链表、二叉树
2.排序和查找算法

三、模板和STL
1.函数模板
2.类模板
3.STL

四、阶段项目
1.简化的企业管理信息系统(MIS)

第一章 C与C++

一、C++由来

   废话不多说,在C的基础上,一九八三年又由贝尔实验室的Bjarne Strou-strup推出了C++。 C++进一步扩充和完善了C语言,成为一种面向 对象的程序设计语言。C++目前流行的集成开发环境最新版本是Borland C++4.5,Symantec C++6.1,和Microsoft VisualC++2017。C++提出了一些更为深入的概念,它所支持的这些面向对象的概念容易将问题空间直接地映射到程序空间,为程序员提供了一种与传统结构程序设计不同的思维方式和编程方法。因而也增加了整个语言的复杂性,掌握起来有一定难度。

二、C++语言的使用领域:

  1. 游戏开发:强建模能力,性能高。
  2. 科学计算:FORTRAN,C++算法库。
  3. 网络和分布式:ACE框架。
  4. 桌面应用:VC/MFC,Office,QQ,多媒体
  5. 操作系统和设备驱动:优化编译器的发明使C++在底层开发方面可以和C向媲美。
  6. 移动终端开发

三、C++比C更丰富

  1. 支持面向对象,将问题域和方法域统一化。宏观面向对象,微观面向过程。
  2. 支持泛型编程。
    int add (int a, int b) { ... } template<typename T> T add (T a, T b) { ... }
  3. 支持异常机制
int func (void) {
  ...
}
int main (void) {
  if (func () == -1) {
     错误处理;
  }
}
  1. 操作符重载

四、第一个C++程序

  1. 编译器:g++,如果用gcc需要带上-lstdc++,指定其使用标准c++的运行库。
  2. 源文件扩展名:.cpp/.cc/.C/.cxx/.c++,最好用.cpp。
  3. 头文件:#include 大多数标准库头文件都没有.h后缀。
  4. 输出:cout - 标准输出对象
    输入:cin - 标准输入对象
    插入运算符:<<
    提取运算符:>>
  5. std:所有标准库的函数、对象、类型都位于std名字空间中。

五、名字空间

1.对程序中的标识符(类型、函数、变量)

  • 按照某种逻辑规则划分成若干组。

2.定义名字空间

  • namespace 名字空间名 {
    名字空间成员;
    }

3.使用名字空间

  • 1.作用于限定符:名字空间名::名字空间成员,表示访问特定名字空间中的特定成员。
    例:
#include <iostream>

int main (void) {

	std::cout << "Hello, World !" << std::endl;

	int i;

	double d;

	char s[256];

   //	scanf ("%d%lf%s", &i, &d, s);

	std::cin >> i >> d >> s;

   //	printf ("%d %lf %s\n", i, d, s);

	std::cout << i << ' ' << d << ' ' << s << '\n';

	return 0;

}
  • 2.名字空间指令:using namespace 名字空间名;在该条指令之后的代码对指令所指名字空间中的所有成员都可见,
    可直接访问这些成员,无需加“::”。
  • 3.名字空间声明:using 名字空间名::名字空间成员;将指定名字空间中的某个成员引入当前作用域,可直接访问这些成员,无需加“::”。
  • 4.匿名名字空间:如果一个标识符没有被显示地定义在任何名字空间中,编译器会将其缺省地置于匿名名字空间中。对匿名名字空间中的成员通过“::名字空间成员”的形式访问。
  • 5.名字空间嵌套:
    例:
namespace fx1 {
  namespace fx2 {
namespace fx3 {
      void foo (void) { ... }
}
}
}
fx1::fx2::fx3::foo ();
using namespace fx1::fx2::fx3;
foo ();

例:名字空间

#include <iostream>
using namespace std;
//namespace {
	void print (int money) {
		cout << money << endl;
	}
//}
// 农行名字空间
namespace abc {
	int balance = 0;
	void save (int money) {
		balance += money;
	}
	void draw (int money) {
		balance -= money;
	}
}
namespace abc {
	void salary (int money) {
		balance += money;
	}
	void print (int money) {
		cout << "农行:";
		::print (money);
	}
}
// 建行名字空间
namespace ccb {
	int balance = 0;
	void save (int money) {
		balance += money;
	}
	void draw (int money) {
		balance -= money;
	}
	void salary (int money) {
		balance += money;
	}
}
int main (void) {
	using namespace abc; // 名字空间指令
	save (5000);
	cout << "农行:" << balance << endl;
	draw (3000);
	cout << "农行:" << balance	<< endl;
	ccb::save (8000);
	cout << "建行:" << ccb::balance << endl;
	ccb::draw (5000);
	cout << "建行:" << ccb::balance << endl;
	using ccb::salary; // 名字空间声明
//	using abc::salary;
	salary (6000);
	cout << "建行:" << ccb::balance << endl;
	abc::print (abc::balance);
	return 0;
}

六、C++中的结构、联合和枚举

1. 结构

  • 定义结构型变量时,可以省略struct关键字。
  • 结构内部可以定义函数——成员函数。
  • sizeof (空结构) -> 1
    例:
#include <iostream>
using namespace std;
struct Student {
	char name[128];
	int age;
	void who (void) {       //成员函数
		cout << "我叫" << name << ",今年" << age
			<< "岁了。" << endl;
	}
};
int main (void) {
	Student student = {"张飞", 25}, *ps = &student;
	student.who ();
	ps->who ();
	struct A {};
	cout << sizeof (A) << endl;
	return 0;
}

2. 联合

  1. 增加了匿名联合的概念。借用联合语法形式,描述一些变量在内存中的布局方式。
int main()
{
	UNION 
	{
   	int a;
   	char ch[4];
	};
 	a=0x12345678;
}
定义联合变量时,可以不加union

例:

#include <iostream>
using namespace std;
int main (void) {
	// 匿名联合
	union {
		int x;
		char c[4] /*= {'A', 'B', 'C', 'D'}*/;
	};
	cout << (void*)&x << ' ' << (void*)c << endl;
	x = 0x12345678;
	for (int i = 0; i < 4; ++i)
		cout << hex << (int)c[i] << ' ';
	cout << endl;
	return 0;
}

3.枚举( 枚举是一个独立的数据类型 )

C:
enum E {a, b, c};
enum E e;
e = a;
e = 1000;
C++:
enum E {a, b, c};
E e;
e = a;
e = b;
e = c;
b=1;      // ERROR !
e = 1000; // ERROR !
e = 1;    // ERROR !

例:

#include <iostream>
using namespace std;
int main (void) {
	enum E {a, b, c};
	E e;
	e = a;
	e = b;
	e = c;
  //	e = 1000;//报错
  //	e = 1;   //报错     
  	return 0;
}

七、C++中布尔型变量

注:跟在cout 后面时可以 boolalpha

bool b = true;
b = false;
cout << sizeof (b) << endl; // 1
b = 100;
b = 1.234;
b = "hello";
b = 'A';

八、C++中的运算符别名

  • && - and
  • || - or
  • & - bitand
  • ^ - xor
  • { - <%
  • } - %>
  • [ - <:
  • ] - :>

九、C++中的函数

1. 重载的条件:

  • 在同一个作用域中;
  • 函数名相同;
  • 参数表不同的函数;
  • 构成重载关系。

C++编译器会对程序中的函数做换名,将参数表中的类型信息汇合到函数名中,以保证函数名的唯一。通过extern “C”,可以要求编译器不做C++换名,以方便在C语言的模块中使用C++编译生成的代码。

方式一:
extern "C" {
int add (int a, int b) {
	return a + b;
  }
int sub (int a, int b) {
	return a - b;
  }
           }
方式二:
extern "C" int add (int a, int b, int c) {
	return a + b + c;        }

2. 缺省参数和哑元参数

  1. 如果调用一个函数时,没有提供实参,那么对应形参就取缺省值。
  2. 如果一个参数带有缺省值,那么它后边的所有参数必须都带有缺省值。
  3. 如果一个函数声明和定义分开,那么缺省参数只能放在声明中。
  4. 避免和重载发生歧义。
  5. 只有类型而没有名字的形参,谓之哑元。
i++ - operator++
++i
V1: void decode (int arg) { ... }
V2: void decode (int) { ... }

例1:重载与缺省值

#include <iostream>
using namespace std;
void foo (int a = 10, double b = 0.01, 
	       const char* c = "tarena");   //函数1
void foo (void) {}                      //函数2
//函数1与函数2构成重载关系
void bar (int) {                        //函数3
	cout << "bar(int)" << endl;
}
void bar (int, double) {                //函数4
	cout << "bar(int,double)" << endl;
}
//函数3与函数4构成重载关系
int main (void) {
	foo (1, 3.14, "hello");//调用函数1
	foo (1, 3.14);         //调用函数1

	foo (1);               //调用函数1 
//	foo (); // 歧义 ,可以调用函数2,但也可以调用函数1,因为函数1在不提供实参的情况下,可以取缺省值。
	bar (100);             //调用函数3
	bar (100, 12.34);      //调用函数4
	return 0;
}

例2:重载与作用域

#include <iostream>
using namespace std;
namespace ns1 {
	int foo (int a) {                       函数1
		cout << "ns1::foo(int)" << endl;
		return a;
	}
};
namespace ns2 {
	double foo (double a) {                 函数2
		cout << "ns2::foo(double)" << endl;
		return a;
	}
};
int main (void) {
	using namespace ns1;  // 名字空间指令
	using namespace ns2;  // 名字空间指令
	cout << foo (10) << endl;    //10 调用函数1,作用域可见ns2与ns1,所以与函数2构成重载
	cout << foo (1.23) << endl;  //1.23 调用函数2,作用域可见ns2与ns1,所以与函数1构成重载



	using ns1::foo;              //名字空间声明
(当同时出现名字指令与名字空间声明,则名字空间声明会隐藏名字空间指令)
	cout << foo (10) << endl;    //10,调用函数1,只可见名字空间ns1的foo(),所以也并不构成重载。
	
   cout << foo (1.23) << endl;  //10,调用函数1,只可见名字空间ns1的foo(),所以也并不构成重载。

	using ns2::foo;              //名字空间声明
	cout << foo (10) << endl;    //10,调用函数1,可见名字空间ns1与名字空间ns2的foo(),所以构成重载。
	
  cout << foo (1.23) << endl;  //1.23,调用函数2,可见名字空间ns1与名字空间ns2的foo(),所以构成重载。
	return 0;
}

3. 内联

  1. 编译器用函数的二进制代码替换函数调用语句,减少函数调用的时间开销。这种优化策略成为内联。
  2. 频繁调用的简单函数适合内联,而稀少调用的复杂函数不适合内联。
  3. 递归函数无法内联。
  4. 通过inline关键字,可以建议编译对指定函数进行内联,但是仅仅是建议而已。
inline void foo (int x, int y){...}

十、C++的动态内存分配

  • malloc/calloc/realloc/free
  • 1.new/delete:对单个变量进行内存分配/释放
  • 2.new[]/delete[]:对数组进行内存分配/释放

例:new与delete

#include <iostream>
using namespace std;
int main (void) {
  //	int* pi = (int*)malloc (sizeof (int));
  //	free (pi);         //c中的方法
	int* pi = new int;
	*pi = 1000;
	cout << *pi << endl;
	delete pi;               //一定要释放内存,否则会造成内存泄露,很严重
	pi = NULL;               //不要忘记这个,虽然不会报错,但是要有好习惯
	/*
	*pi = 2000;  
	cout << *pi << endl;     //pi指向的内存地址已经被释放,被初始化为指向NULL
	*/
   pi = new int[10];
	for (size_t i = 0; i < 10; ++i)
		pi[i] = i;
	for (size_t i = 0; i < 10; ++i)
		cout << pi[i] << ' ';
	cout << endl;
	delete[] pi;            //千万记住new[]要用delete[]来释放内存
	pi = NULL;
   pi = new int (1234);     //用new分配内存的同时初始化赋一个值。
	cout << *pi << endl;    //1234
	delete pi;
	pi = NULL;
   char buf[4] = {0x12,0x34,0x56,0x78};
	pi = new (buf) int;
	cout << hex << *pi << endl;
//	delete pi;
   cout << (void*)pi << ' ' << (void*)buf << endl;
	int (*p)[4] = new int[3][4];
	delete[] p;
	int (*q)[4][5] = new int[3][4][5];
	delete[] q;
	return 0;
}

十一、引用

1. 引用即别名

int a = 20;
int& b = a; // int* b = &a;
b = 10; // *b = 10;
cout << a << endl; // 10

2. 引用必须初始化

int a;
int* p;
a = 20;
p = &a;
int& b; // ERROR !
int& b = a; // OK

3.引用一旦初始化就不能再引用其它变量。

int a = 20, c = 30;
int& b = a;
b = c; // c => b/a

4.引用的应用场景

1)引用型参数

  • a.修改实参
  • b.避免拷贝,通过加const可以防止在函数中意外地修改实参的值,同时还可以接受拥有常属性的实参。

2)引用型返回值

int b = 10;
int a = func (b);
func (b) = a;

从一个函数中返回引用往往是为了将该函数的返回值作为左值使用。但是,一定要保证函数所返回的引用的目标在该函数返回以后依然有定义,否则将导致不确定的后果。不要返回局部变量的引用,可以返回全局、静态、成员变量的引用,也可以返回引用型形参变量本身。

5.引用和指针

1)引用的本质就是指针,很多场合下引用和指针可以互换。
2)在C++层面上引用和指针存在以下不同:

  • 指针式实体变量,但是引用不是实体变量。
  • 指针可以不初始化,但是引用必须初始化。
  • 指针的目标可以修改,但是引用的目标的不能修改。
  • 可以定义指针的指针,但是不能定义引用的指针。
  • 可以定义指针的引用,但是不能定义引用的引用。
  • 可以定义指针的数组,但是不能定义引用的数组。

例:指针与引用

#include <iostream>
using namespace std;
void swap1 (int a, int b) {
	int c = a;
	a = b;
	b = c;
}
void swap2 (int* a, int* b) {
	int c = *a;
	*a = *b;
	*b = c;
}
void swap3 (int& a, int& b) {
	int c = a;
	a = b;
	b = c;
}
void swap4 (const char* x, const char* y) {
	const char* z = x;
	x = y;
	y = z;
}
void swap5 (const char** x, const char** y) {
	const char* z = *x;
	*x = *y;
	*y = z;
}
void swap6 (const char*& x, const char*& y) {
	const char* z = x;
	x = y;
	y = z;
}
struct Student {
	char name[1024];
	int age;
};
void print (const struct Student& s) {
	cout << s.name << "," << s.age << endl;
//	s.age = -1;
}
void foo (const int& x) {
	cout << x << endl;
}
int main (void) {
	int a = 100, b = 200;
//	swap1 (a, b);
//	swap2 (&a, &b);
	swap3 (a, b);
	cout << a << ' ' << b << endl; // 200 100
	const char* x = "hello", *y = "world";
//	swap4 (x, y);
//	swap5 (&x, &y);
	swap6 (x, y);
	cout << x << ' ' << y << endl;
	Student s = {"张飞", 22};
	print (s);
	print (s);
	foo (100);
	return 0;
}

十二、显示类型转换运算符

C:目标类型变量 = (目标类型)源类型变量;

1. 静态类型转换
static_cast<目标类型> (源类型变量)
如果在目标类型和源类型之间某一个方向上可以做隐式类型转换,那么在两个方向上都可以做静态类型转换。反之如果在两个方向上都不能做隐式类型转换,那么在任意一个方向上也不能做静态类型转换。

int* p1 = ...;
void* p2 = p1;
p1 = static_cast<int*> (p2);
char c;
int i = c;
如果存在从源类型到目标类型的自定义转换规则,
那么也可以使用静态类型转换。

例:静态类型转换

#include <iostream>
using namespace std;
int main (void) {
	int* p1 = NULL;               //p1为int型的指针
	void* p2 = p1;                //p2为void型的指针
	p1 = static_cast<int*> (p2);   //将void型的p2指针转换为int型指针并复制给int型的p1指针。
	return 0;
}

2. 动态类型转换

dynamic_cast<目标类型> (源类型变量)
用在具有多态性的父子类指针或引用之间。

3. 常类型转换

const_cast<目标类型> (源类型变量)
给一个拥有const属性的指针或引用去常

const int a = 100;
const int* p1 = &a;
*p1 = 200; // ERROR
int* p2 = const_cast<int*> (p1);
*p2 = 200; // OK

4.从解释类型转换

reinterpret_cast<目标类型> (源类型变量);
在不同类型的指针或引用之间做类型转换,以及在指针和整型之间做类型转换。

例:常类型转换

#include <iostream>
using namespace std;
int main (void) {
	const volatile int a = 100;
//	a = 200;
	const volatile int* p1 = &a;
//	*p1 = 200;
	int* p2 = const_cast<int*> (p1); //去常,去掉常属性
	*p2 = 200;
	cout << *p2 << endl;   // 200
	cout << a << endl;     // 200
	// cout << 100 << endl;
	return 0;

}

关键字volatile在描述变量时使用,阻止编译器优化那些以valatile修饰的变量,volatile被用在一些变量能被意外方式改变的地方,例如:抛出中断,这些变量若无volatile可能会和编译器执行的优化 相冲突。

例:解释型类型转换

#include <iostream>
using namespace std;
int main (void) {
	int i = 0x12345678;
	char* p = reinterpret_cast<char*> (&i);
	for (size_t i = 0; i < 4; ++i)
		cout << hex << (int)p[i] << ' ';  //78 56 34 12
	cout << endl;
	float* q = reinterpret_cast<float*> (&i);
	cout << *q << endl;
	void* v = reinterpret_cast<void*> (i);
	cout << v << endl;
	return 0;
}

十三、C++中的注意事项

  1. 少用宏,多用const、enum和inline
#define PAI 3.141519
const double PAI = 3.14159;
#define ERROR_FILE -1
#define ERROR_MEM  -2
enum {
  ERROR_FILE = -1,
  ERROR_MEM = -2
};
#define max(a,b) ((a)>(b)?(a):(b))
inline int double max (double a, double b) {
  return a > b ? a : b;
}
  1. 变量随用随声明同时初始化。
  2. 少用malloc/free,多用new/delete。
  3. 少用C风格的强制类型转换,多用类型转换运算符。
  4. 少用C风格的字符串,多用string。
  5. 树立面向对象的编程思想。

第二章 类和对象

一、什么是对象

  1. 万物皆对象
  2. 程序就是一组对象,对象之间通过消息交换信息
  3. 类就是对对象的描述和抽象,对象就是类的具体化和实例化

二、通过类描述对象

  • 属性:姓名、性别、学号
  • 行为:吃饭、学习、打游戏

类就是从属性和行为两个方面对对象进行抽象。

三、面向对象程序设计(OOP)

  1. 对象 -> 抽象 -> 类 -> 对象
  2. 至少掌握一种OOP编程语言
  3. 精通一种面向对象的元语言—UML(后续博客会介绍)
  4. 研究设计模式,GOF

四、类的基本语法

1. 类的定义

class 类名 {
};
如
class Student {
};

2. 成员变量——属性

class 类名 {
  类型 成员变量名;
};
如
class Student {
  string m_name;
  int    m_age;
};

3. 成员函数——行为

class 类名 {
  返回类型 成员函数名 (形参表) {
    函数体;
  }
};
如
class Student {
  string m_name;
  int    m_age;
  void eat (const string& food) {
    ...
  }
};

4. 访问控制属性
1)公有成员:public,谁都可以访问。
2)私有成员:private,只有自己可以访问。
3)保护成员:protected,只有自己和自己的子类可以访问。
4)类的成员缺省访控属性为私有,而结构的成员缺省访控属性为公有。

例:

#include <iostream>
using namespace std;
class Student {
private:          //声明为私有部分
	string m_name;
	int m_age;
public:           //声明为私有部分
	void eat (const string& food) {
		cout << m_age << "岁的" << m_name
			<< "同学正在吃" << food << "。" << endl;
	}
	void setName (const string& name) {  //为接口
		if (name == "2")
			cout << "你才" << name << "!" << endl;
		else
			m_name = name;
	}
	void setAge (int age) {              //为接口
		if (age < 0)
			cout << "无效的年龄!" << endl;
		else
			m_age = age;
	}
};
int main (void) {
	Student student;
   student.setName ("2");    //你才2
   student.setAge (-100);    //无效年龄
   student.setName ("张飞"); //将其赋值给成员变量m_name
   student.setAge (20);      //将其赋值给成员变量m_age
	student.eat ("包子");     //20岁的张飞同学正在吃包子
	return 0;
}

5.构造函数

class	{
  ...
  类名 (行参表) {
    构造函数体;
  }
};
当一个对象被创建时,构造函数会自动被执行,其参数来自构造实参。
  • 构造函数可以通过构造参数实现重载。
  • 如果一个类没有定义任何构造函数,那么系统就会缺省地为其提供一个无参构造函数,该构造函数对于基本类型的成员变量不做初始化,对于类类型的成员变量,调用其相应类型的无参构造函数初始化。

五、this指针

  1. 含义:在类的构造函数或成员函数中,关键字this表示一个指针,对于构造函数而言,this指向正在被构造的对象,对于成员函数而言,this指向调用该函数的对象。
  2. this指针的用途
    1)在类的内部区分成员变量。
    2)在成员函数中返回调用对象自身。
    3)在成员函数内部通过参数向外界传递调用对象自身,以实现对象间交互。
class A {
   B m_b;
};
class B {
   A m_a;
};
sizeof(A)  //error
class C {
   C m_c;
};

例1:

#include <iostream>
using namespace std;
class A {
public:
	A (int data) : data (data) {
		cout << "构造: " << this << endl;
//		this->data = data;
	}
	void foo (void) {
		cout << "foo: " << this << endl;
		cout << this->data << endl;
	}
	int data;
};
int main (void) {
 A a (1000);                //创建对象调用了构造函数,并输出this的地址,输出“构造:0xbf9b24d8”
cout << "main: " << &a << endl;//输出该对象的地址,输出“main:0xbf9b24d8”
a.foo ();                   //该对象调用foo函数,输出this的值,以及输出this->data的值,输出“foo:0xbf9b24d8  1000”
A* pa = new A (1000);       //创建对象调用构造函数,输出this的地址,输出“构造:0x95cc008”
cout << "main: " << pa << endl; //输出该对象的地址,输出“main:0x95cc008”
pa->foo ();                 //该对象调用foo函数,输出this以及this->data的值,输出“foo:0x95cc008  1000”
delete pa;
}

例2:

#include <iostream>
using namespace std;
class Counter {
public:
	Counter (void) : m_data (0) {}
	Counter& inc (void) {  //返回的是一个别名,不加&的话,返回的就是一个拷贝
		++m_data;
		return *this;
	}
	void print (void) {
		cout << m_data << endl;
	}
private:
	int m_data;
};
int main (void) {
	Counter c;
//	c.inc ();
//	c.inc ();
//	c.inc ();
	c.inc ().inc ().inc ();//函数返回的是一个别名,是一个左值,可以用来调用函数
	c.print ();            // 输出为3,如果前面的函数不加&,返回的只是拷贝,输出为1。
	return 0;
}

例3:

#include <iostream>
using namespace std;
class Student;   //因为在Teacher中会用到Student,所以提前声明一下
class Teacher {
public:
	void educate (Student* s);//可以声明在类的内部,定义在类的外部
	void reply (const string& answer) {
		m_answer = answer;
	}
private:
	string m_answer;
};
class Student {
public:
	void ask (const string& question, Teacher* t) {
		cout << "问题:" << question << endl;
		t->reply ("不知道。");
	}
};
void Teacher::educate (Student* s) {
	s->ask ("什么是this指针?", this);//将问题question和Teacher类变量的地址作为参数传递给Student类中的ask成员函数,并在ask函数中得到一个值作为参数传递给Teacher类中的replay函数,将值赋给m_answer,最后完成输出。
	cout << "答案:" << m_answer << endl;
}
int main (void) {
	Teacher t;
	Student s;
	t.educate (&s);
	return 0;
}

六、常函数与常对象

  1. 如果在一个类的成员函数的参数表后面加上const关键字,那么这个成员函数就被成为常函数,常函数的this指针是一个常指针。在常函数内部无法修改成员变量,除非该变量具有mutable属性。而且在常函数内部也无法调用非常函数。
  2. 常对象:拥有const属性的对象,兑现引用或指针。
    注:常对象只能调用常函数。

const X1 函数名 (const Y1 y1)
const {

}

例:

#include <iostream>
using namespace std;
class A {
public:
//	void bar (void) {            //函数1
//		cout << "非常bar" << endl;
//	}
//函数1与函数2构成函数重载
	void bar (void) const {      //函数2
		cout << "常bar" << endl;
	}
//  void XXXbarYYY (A* this) {}
	  void foo (void) const {      //函数3
//		m_i = 100;  //在常函数内部无法修改成员变量,除非那个成员变量有mutable属性,例:mutable int m_i;
		const_cast<A*>(this)->m_i = 100;//也可以通过去常来解决
	}
	void print (void) const {
		cout << m_i << endl;
	}
//	_ZNK1A3fooEv (const A* this) {
//		const_cast<A*>(this)->m_i = 100;
//	}
	int m_i;
};
void func (void) /*const*/ {}
int main (void) {
	A a;
	a.foo ();  //调用的是函数3
	a.print ();//“100”
	
  const A& r = a;//r为常对象,a为非常对象
	r.bar ();//“常bar”,r为常对象,常对象只能调用常函数
//	XXXbarYYY (&r); // const A*
	
  a.bar ();//“常bar”,a为非常对象,可以调用常函数,也可调用非常函数
//	XXXbarYYY (&a); // A*
	return 0;
}

七、析构函数

class 类名 {
  ~类名 (void) {
     析构函数体;
  }
};

析构函数名也应与类名相同,只是在函数名前面加一个位取反符~,以区别于 构造函数。它不能带任何参数,也没有返回值(包括void类型)。只能有一个析构函数,不能 重载。如果用户没有编写析构函数, 编译系统会自动生成一个缺省的析构函数(即使自定义了析构函数, 编译器也总是会为我们合成一个析构函数,并且如果自定义了析构函数,编译器在执行时会先调用自定义的析构函数再调用合成的析构函数),它也不进行任何操作。所以许多简单的类中没有用显示的析构函数。

一般情况下,在析构函数中释放各种动态分配的资源。

构造: 基类->成员->子类
析构: 子类->成员->基类

例:

#include <iostream>
using namespace std;
class Double {
public:
	Double (double data) :
		m_data (new double (data)) {
		cout << "构造" << endl;
	} //构造函数早开始时执行,既创建对象时执行
	~Double (void) {
		cout << "析构" << endl;
		delete m_data;
	}//析构函数在对象结束时执行
	void print (void) const {
		cout << *m_data << endl;
	}
private:
	double* m_data;
	string m_lable;
};
int main (void) {
//	{
		Double d1 (3.14);
		d1.print ();//“构造,3.14”(d1调用的构造)
//	}
	Double* d2 = new Double (1.23);//“构造”(d2调用的构造)
	delete d2;     //“析构”(d2调用的析构)
	cout << "再见!" << endl;//“再见”
	return 0;      //“析构”,(程序结束,d1调用的析构)
}

八、拷贝构造函数和拷贝赋值运算符

1. 拷贝构造函数

  • 拷贝构造:用一个已有的对象,构造和它同类型的副本对象——克隆。
  • 拷贝构造函数:如果一个类没有定义拷贝构造函数,系统会提供一个缺省拷贝构造函数。缺省拷贝构造函数对于基本类型的成员变量,按字节复制,对于类类型的成员变量,调用相应类型的拷贝构造函数。
  • 在某些情况就下,缺省拷贝构造函数只能实现浅拷贝,如果需要获得深拷贝的复制效果,就需要自己定义拷贝构造函数。

例:拷贝函数

#include <iostream>
using namespace std;
class Integer {
public:
	Integer (int data = 0) : m_data (data) {}//构造函数
	void print (void) const {
		cout << m_data << endl;
	}
	
//拷贝构造(自己定义的):
	Integer (const Integer& that) :
		m_data (that.m_data) {
		cout << "拷贝构造" << endl;
	}
	
private:
	int m_data;
};
void foo (Integer i) {    //用Inerger类变量时实参给函数中Interger类的形参赋值,同样会调用拷贝构造函数
	i.print ();
}
Integer bar (void) {
	Integer i;
	return i;
}
int main (void) {
	Integer i1 (10);
	i1.print ();//正常创建对象,输出“10”
	Integer i2 (i1); // 调用拷贝构造,输出“拷贝构造”
	i2.print ();  //调用print函数,输出“10”
	Integer i3 = i1; // 调用拷贝构造,输出“拷贝构造”
	i3.print ();  //调用print函数,输出“10”
//	Integer i4 (10, 20);
	cout << "调用foo()函数" << endl;
	foo (i1);     //调用拷贝构造函数,且调用print函数输出,所以输出为“拷贝构造  10”
	cout << "调用bar()函数" << endl;
	Integer i4 (bar ());
	return 0;
}

4.拷贝赋值运算符函数

形如
class X {
  X& operator= (const X& that) {
    ...
  }
};

中的成员函数称为拷贝赋值运算符函数。如果一个类没有定义拷贝赋值运算符函数,系统会提供一个缺省拷贝赋值运算符函数。缺省拷贝赋值运算符函数对于基本类型的成员变量,按字节复制,对于类类型的成员变量,调用相应类型的拷贝赋值运算符函数。
在某些情况就下,缺省拷贝赋值运算符函数只能实现浅拷贝,如果需要获得深拷贝的复制效果,就需要自己定义拷贝赋值运算符函数。

例:拷贝赋值运算符函数

#include <iostream>
using namespace std;
class Integer {
public:
	Integer (int data) : m_data (new int (data)) {}
//构造函数
	~Integer (void) {    //析构函数
		if (m_data) {
			delete m_data;
			m_data = NULL;
		}
	}
	void print (void) const {
		cout << *m_data << endl;
	}
	Integer (const Integer& that) :   //拷贝构造函数
		m_data (new int (*that.m_data)) {}
	void set (int data) {
		*m_data = data;
	}
//拷贝赋值运算符函数(运算符重载)
	Integer& operator= (const Integer& that) {
		   // 防止自赋值
		if (&that != this) {
			// 释放旧资源
			delete m_data;
			// 分配新资源
			m_data = new int (*that.m_data);
			// 拷贝新数据
		}
	   	// 返回自引用
		return *this;
	}
private:
	int* m_data;
};
int main (void) {
	Integer i1 (10);
	i1.print ();
	Integer i2 (i1);
	i2.print ();
	i2.set (20);
	i2.print ();
	i1.print ();
	Integer i3 (30);
	i3.print (); // 30
	i3 = i1; // 拷贝赋值
	// i3.operator= (i1);
	i3.print (); // 10
	i3.set (40);
	i3.print (); // 40
	i1.print (); // 10
	/*
	int a = 10, b = 20, c = 30;
	(a = b) = c;
	cout << a << endl;
	*/
	(i3 = i1) = i2;
//	i3.operator=(i1).operator=(i2);
	i3.print ();
	i3 = i3;
	i3.print ();
	return 0;
}

九、静态成员

  • 静态成员变量和静态成员函数是属于类的而非属于对象。
  • 静态成员变量,为多个对象所共享,只有一份实例,可以通过对象访问也可以通过类访问,必须在类的外部定义并初始化。静态成员变量本质上与全局变量并没有区别,只是多了类作用域的约束,和访控属性的限制。
class Account {
private:
  string m_name;
  double m_balance;
  static double m_rate;
};
  • 静态成员函数,没有this指针,无法访问非静态成员。
  • 单例模式

单例模式,可以说设计模式中最常应用的一种模式了,据说也是面试官最喜欢的题目。但是如果没有学过设计模式的人,可能不会想到要去应用单例模式,面对单例模式适用的情况,可能会优先考虑使用全局或者静态变量的方式,这样比较简单,也是没学过设计模式的人所能想到的最简单的方式了。

一般情况下,我们建立的一些类是属于工具性质的,基本不用存储太多的跟自身有关的数据,在这种情况下,每次都去new一个对象,即增加了开销,也使得代码更加臃肿。其实,我们只需要一个实例对象就可以。如果采用全局或者静态变量的方式,会影响封装性,难以保证别的代码不会对全局变量造成影响。

考虑到这些需要,我们将默认的构造函数声明为私有的,这样就不会被外部所new了,甚至可以将析构函数也声明为私有的,这样就只有自己能够删除自己了。在Java和C#这样纯的面向对象的语言中,单例模式非常好实现,直接就可以在静态区初始化instance,然后通过getInstance返回,这种就被称为饿汉式单例类。也有些写法是在getInstance中new instance然后返回,这种就被称为懒汉式单例类,但这涉及到第一次getInstance的一个判断问题。

例1:

#include <iostream>
using namespace std;
class A {
public:
	static int m_i;
	static void foo (void) {
		cout << "foo:" << m_i << endl;
//		m_d = 3.14;//报错,静态成员函数不能访问非静态成员成员
//		bar ();    //报错,理由同上
	}
	double m_d;
	void bar (void) {
		m_i = 1000;//OK,非静态成员函数可以访问非静态成员,也可以访问静态成员
		foo ();   //OK,理由同上
	}
};
int A::m_i = 1;//在外部定义
int main (void) {
	A::m_i = 10;//通过类访问静态成员变量

	A a1, a2;
	cout << ++a1.m_i << endl;//通过对象访问静态成员变量,输出为“11”
	cout << a2.m_i << endl;  //因为静态成员变量,为多个对象共享,只有一个实例,所以上面a1将m_i修改为11,则通过a2访问的m_i也是11,输出为“11”

	A::foo ();//输出为“foo:11”,通过类访问静态成员函数
	a1.foo ();//输出为“foo:11”,通过对象访问静态成员函数
	a1.bar ();//先调用bar,将m_i修改为1000,再调用foo,输出为“foo:1000”
	return 0;
}

例2:单例模式(饿汉方式):

#include <iostream>
using namespace std;
// 饿汉方式
class Singleton {
public:
	static Singleton& getInst (void) {
		return s_inst;  //当调用这个函数时,不会新创建对象,不会调用构造函数,只是返回创建好的那个对象
	}
private:
	Singleton (void) {}          //构造函数
	Singleton (const Singleton&);//拷贝构造函数
	static Singleton s_inst;     //(类的内部)静态成员变量的声明,所有对象公用
};
Singleton Singleton::s_inst;//(类的外部)静态成员变量的定义,这个时候已经创建了对象,并调用构造函数
int main (void) {
	Singleton& s1 = Singleton::getInst ();//不会创建新的对象,返回已经创建好的Singletion类对象,并没有新分配内存和地址
	Singleton& s2 = Singleton::getInst ();//与上面一样,还是返回那个对象,内存地址没有变
	Singleton& s3 = Singleton::getInst ();//还是一样滴
	cout << &s1 << ' ' << &s2 << ' ' << &s3 << endl;
//输出的都是0x804a0d4
	return 0;
}

例3:单例模式(懒汉模式)

#include <iostream>
using namespace std;
// 懒汉方式
class Singleton {
public:
	static Singleton& getInst (void) {
		if (! m_inst)   //m_inst指针被初始化为NULL,第一次调用时,执行下面那一行代码来分配内存创建一个对象。否则跳过那一行代码。一旦执行过一次下一行代码,则m_inst就不会再为NULL,也就是说只会分配一次内存,只有一个对象
		m_inst = new Singleton;//分配内存,创建对象,执行一次
		++m_cn;//调用一次该函数,则数量加一
		return *m_inst;//返回创建好的m_inst
	}
	void releaseInst (void) {
		if (m_cn && --m_cn == 0)//调用了几次getInst,就要调用几次该函数,才能真正把对象释放掉
			delete this;
	}
private:
	Singleton (void) {  //构造函数
		cout << "构造:" << this << endl;
	}
	Singleton (const Singleton&);//析构函数
	~Singleton (void) {
		cout << "析构:" << this << endl;
		m_inst = NULL;
	}
	static Singleton* m_inst;//声明该指针(静态)
	static unsigned int m_cn;//声明该变量(静态)
};
Singleton* Singleton::m_inst = NULL;//先将指针初始化,并没有创建对象,分配内存
unsigned int Singleton::m_cn = 0;//创建对象并初始化,调用构造,这个变量什维利计算m_inst的数量
int main (void) {
	Singleton& s1 = Singleton::getInst ();//第一次调用getInst函数,所以执行哪一行代码来分配内存创建对象,并返回该对象
	Singleton& s2 = Singleton::getInst ();//因为不是第一次调用,m_inst已经不再指向NULL,所以根据条件语句,不执行创建对象的代码,直接返回原先第一次调用还函数时创建好的对象,地址神马的都不变
	Singleton& s3 = Singleton::getInst ();//同上
	cout << &s1 << ' ' << &s2 << ' ' << &s3 << endl;
//输出的地址都是一样的,因为只分配了一次内存,创建了一次对象
	s3.releaseInst ();//调用时,m_cn为3,结束时为2,没有释放掉
	s2.releaseInst ();//调用时,m_cn为2,结束时为1,没有释放掉
	s1.releaseInst ();//调用时,m_cn为1,满足条件语句,执行delete this ,真正释放掉
	return 0;
}

十、成员指针

1. 成员变量指针
1.1定义:
是C++中用于对类中成员进行操作。
成员指针的定义格式:成员类型 类名::*指针名=&类名::成员名;成员函数指针的定义格式: 成员函数返回类型 类名::*指针名 = &类名::成员函数名(参数表);

class A

{

int m;

public:

void func(){};

...

};

1.2初始化/赋值
指针变量名 = &类名::成员变量名
pname = &Student::m_name;
page = &Student::m_age;

1.3.解引用
对象.*指针变量名
对象指针->*指针变量名

Student s, *p = &s;
s.*pname = "张三";
cout << p->*page << endl;

2.成员函数指针
2.1定义:
成员函数返回类型 (类名::*指针变量名) (参数表)

void (Student::*plearn) (const string&) const;

2.22.初始化/赋值:
指针变量名 = &类名::成员函数名;

plearn = &Stduent::learn;

2.3解引用:

(对象.*指针变量名) (实参表);
(对象指针->*指针变量名) (实参表);
(s.*plearn) (“C++”);
(p->*plearn) (“UNIX”);

例:

#include <iostream>
#include <cstring>
using namespace std;
class Student {
public:
	Student (const string& name, int age) : //构造函数
		m_name (name), m_age (age) {}

	double m_weight;
	string m_name;
	Int m_age;
	void learn (const string& lesson) const {
		cout << "我在学" << lesson << endl;
	}
	static void hello (void) {
		cout << "你好!" << endl;
	}
};
int main (void) {
    //成员变量指针使用对象的地址作为基础,然后用成员变量的偏移量得到该对象中的成员变量的绝对地址。
	string Student::*pname = &Student::m_name;//声明一个指向Student类中m_name成员变量的成员变量指针
	void* pv;
	memcpy (&pv, &pname, 4);
	cout << pv << endl;//输出“0x8”

	int Student::*page = &Student::m_age;  //声明一个指向Student类中m_age成员变量的成员变量指针
	memcpy (&pv, &page, 4);
	cout << pv << endl;//输出“0xc”
	Student s ("张飞", 25), *p = &s;//p是指向对象的指针。存的是对象s的地址
	cout << s.*pname << endl;//成员变量指针,输出“张飞”
	cout << p->*page << endl;//成员变量指针,输出“25”

	Student s2 ("赵云", 22);
	cout << s2.*pname << endl;//输出“赵云”

	void (Student::*plearn) (const string&) const =
		&Student::learn;//成员函数指针
	(s.*plearn) ("C++");//对象是用地址提供this的实参,输出“我在学c++”
	(p->*plearn) ("UNIX");//输出“我在学UNIX”

	void (*phello) (void) = Student::hello;
	phello ();//静态的函数就不需要用对象的地址来提供this的实参,输出“你好”
	return 0;
}

第三章 操作符重载

一、操作符标记和操作符函数

1.双目操作符:L#R

成员函数形式:L.operator# ( R ) 左调右参
全局函数形式:::operator# (L, R) 左一右二

2.单目操作符:#O/O#
成员函数形式:O.operator# ()
全局函数形式:::operator# (O)

二、双目操作符

1. +/-/*//
注:操作数在计算前后不变。表达式的值是右值。

例:

#include <iostream> //操作符重载
using namespace std;
class Complex {
public:
	Complex (int r = 0, int i = 0) :
		m_r (r), m_i (i) {}
	void print (void) const {
		cout << m_r << '+' << m_i << 'i' << endl;
	}
	// 第一个const:返回右值,返回的对象不能接受赋值。
	// 第二个const:支持常量型右操作数,可以接受有常属性的变量为参数,将引用声明为常引用才能指向常变量,常引用也可以指向非常变量。
	// 第三个const:支持常量型左操作数,使this指针变为常指针,也可以指向有常属性的变量。
	const Complex operator+ (const Complex& r) const { //操作符重载的成员函数形式:L.operator+(R)
		return Complex (m_r + r.m_r, m_i + r.m_i);
	}
private:
	int m_r;
	int m_i;
	friend const Complex operator- (const Complex&,
		const Complex&);   //将该函数声明为友远这样该函数就可以访问类的私有部分
};
const Complex operator- (const Complex& l,const Complex& r) {           //操作符重载的全局函数形式,::operator-(L,R)
	return Complex (l.m_r - r.m_r, l.m_i - r.m_i);
}
int main (void) {
	const Complex c1 (1, 2);
	c1.print ();
	const Complex c2 (3, 4);
	Complex c3 = c1 + c2; // c3 = c1.operator+ (c2)
	c3.print (); // 4+6i
//	(c1 + c2) = c3;
	c3 = c2 - c1; // c3 = ::operator- (c2, c1)
	c3.print (); // 2+2i
	return 0;
}

2. +=/-=/*=…
注:左变右不变。表达式的值是左值,左操作数的引用。(a += b) = c;

例:

#include <iostream>
using namespace std;
class Complex {
public:
	Complex (int r = 0, int i = 0) :
		m_r (r), m_i (i) {}
	void print (void) const {
		cout << m_r << '+' << m_i << 'i' << endl;
	}
        //成员函数形式:
	Complex& operator+= (const Complex& r) {  //返回的是左操作数的引用,是可以赋值的,所以最前面不加const,调用该函数的左操作数是要改变的也就是this会修改,也就是调用函数的参数被修改,所以{前也不加const。 
		m_r += r.m_r;
		m_i += r.m_i;
		return *this;//返回该调用操作数
	}
        //全局函数形式:(把定义与声明合二为一,因为有friend,所以是全局函数)
	friend Complex& operator-= (Complex& l,
		const Complex& r) {            //第一个参数l为左操作数引用,可被修改,所以不加const,而第二个参数r为右操作数,值不会被修改,所以加const。
		l.m_r -= r.m_r;
		l.m_i -= r.m_i;
		return l;//返回左操作数
	}
private:
	int m_r;
	int m_i;
};
int main (void) {
	Complex c1 (1, 2), c2 (3, 4);
	c1 += c2; // c1.operator+= (c2)
	c1.print (); // 4+6i
	Complex c3 (5, 6);
	(c1 += c2) = c3;//返回的是左操作数c1的引用,把c3赋值给c1.
	c1.print (); // 5+6i
	c1 -= c2; // ::operator-= (c1, c2)
	c1.print (); // 2+2i
	(c1 -= c2) = c3;
	c1.print (); // 5+6i;
	return 0;
}

3. <</>>

int i = 10;
float f = 1.23;
Complex c (...);
cout << c << i << f << endl;
cin >> c;
  • 这里左操作数ostream/istream类型,不能是常量,不能拷贝。
  • 右操作数自定义类型,对于<<可以是常量,对于>>不能是常量。
  • 表达式的值是左操作数的引用。

例:

#include <iostream>
//输入与输出。
using namespace std;
class Complex {
public:
	Complex (int r = 0, int i = 0) :
		m_r (r), m_i (i) {}
	void print (void) const {
		cout << m_r << '+' << m_i << 'i' << endl;
	}
        //全都使用全局函数的形式
	friend ostream& operator<< (ostream& os,  //返回的是左操作数的引用
		const Complex& r) {               //因为右操作数是要输出的,所以右操作数可以是常量,所以声明为常引用,加const,也可做非常变量的引用。
		return os << r.m_r << '+' << r.m_i << 'i';//返回左操作数的引用
	}
	
        friend istream& operator>> (istream& is,//返回的是左操作数的引用
		Complex& r) {                   //因为右操作数是要输入的,所以右操作数不能是常量,不加const
		return is >> r.m_r >> r.m_i;    //返回左操作数的引用
	}
private:
	int m_r;
	int m_i;
};
int main (void) {
	Complex c1 (1, 2), c2 (3, 4);
	cout << c1 << endl << c2 << endl;
//	::operator<<(::operator<<(cout,c1).operator<<(
//		endl),c2).operator<<(endl);
	cin >> c1 >> c2;
	cout << c1 << endl << c2 << endl;  
	return 0;
}

三、单目操作符

1.-(取负)/!/~

意思就是操作数不变。表达式的值是右值。

例:

#include <iostream>
using namespace std;
class Complex {
public:
	Complex (int r = 0, int i = 0) :
		m_r (r), m_i (i) {}
	void print (void) const {
		cout << m_r << '+' << m_i << 'i' << endl;
	}

        //成员函数形式:
	const Complex operator- (void) const {  //只有一个操作数,且作为调用参数,所以没有形参。因为返回的是右值,是常量,所以要加const(第一个const)。操作数不改变,就是this不可改变,也加const(第二个const)。
		return Complex (-m_r, -m_i);//返回的是右值(常数)
	}
        //全局函数形式:
	friend const Complex operator~ (
		const Complex& o) {       //就一个操作数,且不改变,加const
		return Complex (o.m_i, o.m_r);//交换实部与虚部
	}
private:
	int m_r;
	int m_i;
};
int main (void) {
	const Complex c1 (1, 2);
	Complex c2 = -c1; // c2=c1.operator-()
	c2.print (); // -1+-2i
	Complex c3 = ~c1; // c3=::operator~(c1)
	c3.print (); // 2+1i;
	return 0;
}

2.前++/前–和后++/后–

  • 前++/前–

表达式的值是运算以后的值。
表达式的值是左值,操作数的引用。

  • 后++/后–

表达式的值是运算以前的值。
表达式的值是右值。

放在前边就是先加/减,后执行
放在后边就是先执行,后加减

例1:

#include <iostream>
using namespace std;
class Complex {
public:
	Complex (int r = 0, int i = 0) :
		m_r (r), m_i (i) {}
	void print (void) const {
		cout << m_r << '+' << m_i << 'i' << endl;
	}   
        //成员函数形式:
	Complex& operator++ (void) {   //只有一个操作数,且该操作数是改变的,所以前{不加const。而返回的是一个可以改变的左值,既操作数的引用,所以可以改变,最前面不加const。
		++m_r;
		++m_i;
		return *this;
	}
        //全局函数形式:
	friend Complex& operator-- (Complex& o) {  //返回的是操作数本身,既引用。
		--o.m_r;
		--o.m_i;
		return o;       //返回操作数的引用。
	}
private:
	int m_r;
	int m_i;
};
int main (void) {
	Complex c1 (1, 2);
	Complex c2 = ++c1; // c2=c1.operator++() //成员函数形式
	c1.print (); // 2+3i
	c2.print (); // 2+3i
	(++c1) = Complex (10, 20);
	c1.print (); // 10+20i;
	(++++++c1).print (); // 13+23i
	c2 = --c1; // c2=::operator--(c1)  //全局函数形式
	c1.print (); // 12+22i
	c2.print (); // 12+22i
	return 0;
}

例2:

#include <iostream>
using namespace std;
class Complex {
public:
	Complex (int r = 0, int i = 0) :
		m_r (r), m_i (i) {}
	void print (void) const {
		cout << m_r << '+' << m_i << 'i' << endl;
	}
	const Complex operator++ (int) {   //返回的是一个右值,不变,所以最前面家一个const。int是哑元,占个位置,没有实际意义,与有形参的进行匹配。
		Complex old (*this);
		++m_r;
		++m_i;
		return old;
	}
	friend const Complex operator--(Complex& o,            int) {   //操作数会被修改,所以用引用,不加const。Int是占位置的,没有实际意义

		Complex old (o);
		--o.m_r;
		--o.m_i;
		return old;
	}
private:
	int m_r;
	int m_i;
};
int main (void) {
	Complex c1 (1, 2);
	Complex c2 = c1++; // c2=c1.operator++(0) //成员函数形式
	c1.print (); // 2+3i;
	c2.print (); // 1+2i;
//	(c1++) = c2;
//	c1++++++;
	c2 = c1--; // c2=::operator--(c1,0) //全局函数形式
	c1.print (); // 1+2i //c1进行了--,得到新的值。
	c2.print (); // 2+3i //c2得到表达式之前的值。
	return 0;
}

四、其它操作符

1.下标操作符:[]

int arr[10] = { ... };
arr[1] = 10;
cout << arr[1] << endl;
-----------------------
class Array { ... };
Array arr (...);
arr[1] = 10;
cout << arr[1] << endl;
  • 双目操作符,左操作数是一个具有容器特性的对象,右操作数是容器中特定数据元素的索引(基零的下标)。
  • 下标表达式的值可以是左值,也可以是右值,由容器对象的常属性决定。常容器下标表达式的值是右值,非常容器下标表达式的值是左值。

例:#include
using namespace std;
class Array {
public:
Array (size_t size = 1) : //构造函数
m_data (new int[size]) {}

~Array (void) { //析构函数
if (m_data) {
delete m_data;
m_data = NULL;
}
}
int& operator[] (size_t i) { //不带有常属性的容器对象,没有常属性,所以最前面不加const,返回的是一个引用,可以修改,所以{前不加const
return m_data[i]; //返回类型为该数组中第i个元素的引用
}
const int& operator[] (size_t i) const { //带有常属性的容器对象。是个常量,所以最前面加const,因为返回的是一个不可修改的右值,所以加了第二个const。
return const_cast<Array&> (this)[i]; //去常,转换成不具有常属性,可以调用上面那个重载。
}
private:
int
m_data;
};
int main (void) {
Array arr (10);//不具有常属性
for (size_t i = 0; i < 10; ++i)
arr[i] = i; // arr.operator = i;
arr[0]++;//没有常属性,可以修改
const Array& cr = arr;//具有常属性
for (size_t i = 0; i < 10; ++i)
cout << cr[i] << ’ ';
cout << endl;
// cr[0]++; //具有常属性,不可修改,所以报错。
return 0;
}

2.函数操作符:()

  • 如果为一个类定义了形如:
  • 返回类型 operator() (形参表) {…}
  • 的操作符函数,那么这个类所实例化的对象就可以被当做函数使用。

例:

include <iostream>
using namespace std;
class Square {
public:
	double operator() (double x) {
		return x * x;
	}
};
class Integer {
public:
	explicit Integer (int i = 0) : m_i (i) {} //explicit强制要求用这个构造函数进行类型转换时必须要用显式类型转换
	
        void print (void) const {
		cout << m_i << endl;
	}
	Integer& operator() (int i) {
		m_i += i;
		return *this;//返回自引
	}

	Integer& operator, (int i) {
		m_i += i;
		return *this;
	}

	operator int (void) const {    //可以将Integer转换为int
		return m_i;
	}
private:
	int m_i;
};
void foo (const Integer& i) {
	i.print ();
}
Integer bar (void) {
	return Integer (300);//因为返回的是Integer,所以将300转换为Integer类型的
}
int main (void) {
	Square square;
	cout << square (3) << endl;//这个类的对象可以直接当函数使用。输出为“9”
//	cout << square.operator() (3) << endl;
	Integer i (10);
	i (1) (2) (3) (17);//10+1+2+3+17
	i.print (); // 33
	i, 1, 2, 3, 17;    //33+1+2+3+17
	i.print (); // 56
	i = (Integer)100;//i是Integer类型的,所以要将100进行转换
	i.print ();
	foo (static_cast<Integer> (200));//静态类型转换
	bar ().print ();
	int n = i;
	cout << n << endl;
	return 0;
}

3.解引用(*)和间接访问(->)操作符

  • 以指针的方式使用类类型的对象。

例:

#include <iostream>
#include <memory>
using namespace std;
class A {
public:
	A (void) {
		cout << "构造" << endl;
	}
	~A (void) {
		cout << "析构" << endl;
	}
	void hello (void) {
		cout << "Hello, World !" << endl;
	}
};
class PA {
public:
	PA (A* p = NULL) : m_p (p) {}
	~PA (void) {                  //智能指针,在栈内的mp消灭时,同时将它所指向的堆中的内存释放(delete)
		if (m_p) {
			delete m_p;
			m_p = NULL;
		}
	}

	A& operator* (void) const {    //返回引用
		return *m_p;
	}
	A* operator-> (void) const {   //返回指针,既地址
//		return &**this;       //调用上面的重载函数
		return m_p;           //返回的只是一个地址
	}

	PA (PA& that) : m_p (that.release ()) {}//拷贝构造函数,将自己指向NULL,并将自己指向的内存返回
	PA& operator= (PA& that) {
		if (&that != this)
			reset (that.release ());//reset的参数为that所返回的内存地址
		return *this;
	}
private:
	A* release (void) {
		A* p = m_p;//先将内存保存下来
		m_p = NULL;//
		return p;
	}
	void reset (A* p) {
		if (p != m_p) {
			delete m_p;
			m_p = p;
		}
	}
	A* m_p;
};
void bar (auto_ptr<A> pa) {
	cout << "bar:";
	pa -> hello ();
}
void foo (void) {
	PA pa (new A);//PA保存分配的内存的地址,当在栈中的PA销毁时,调用PA的析构函数,自动执行其中的代码将其delete把内存释放,是智能指针

//  	A* pa = new A;
//	pa -> hello ();
//      (*pa).hello ();
//	A* pb = pa;
        pa -> hello (); // pa.operator->()->hello();//pa返回的只是一个地址,所以用“->”
	(*pa).hello (); // pa.operator*().hello(); //(*pa)返回的是引用,所以可以用“.”访问
	PA pb = pa;//如果没有自己写的拷贝赋值函数,则会出现浅拷贝,出现double free,因为有两个指针都指向同一个内存地址,所以会执行两次析构函数,会出问题。
       //上面自己写的拷贝构造函数,使得pa指向NULL,并返回自己指向的内存地址
       //然后调用操作符重载函数(“=”的),先看pa指向的内存与执行拷贝构造所返回的内存(pb原先指向的内存)是否相同,不相同,则将pb所指向的内存释放,再使pb指向pa原先指向的内存
	pb -> hello ();
	auto_ptr<A> pa1 (new A);//auto_ptr只能用于单个变量,不能用于数组,但smart_ptr都可以
	pa1 -> hello ();
	auto_ptr<A> pa2 = pa1;
	(*pa2).hello ();
	bar (pa2);//实参与bar函数中的形参类似与(PA pb=pa),执行后,pa2就指向了NULL,用不了了,gai内存的析构有bar函数中的形参来实现
	cout << pa2.get () << endl;
}
// smart_ptr
int main (void) {
	foo ();
	return 0;
}

4.自定义类型转换和类型转换操作符

  1. 通过单参构造实现自定义类型转换
    如果A类中有一个可以接受B类对象做为唯一参数的构造函数,那么B类型的对象就可以根据该构造函数被转换为A类型。通过explicit关键字,可以强制使用该构造函数所完成的类型转换必须显示进行。
  2. 通过类型转换操作符函数实现自定义类型转换
    如果A类中有一个形如
    operator B (void) const { … }
    的操作符函数,那么A类型的对象就可以根据该函数被转换为B类型。
  3. 如果目标类型是类类型,源类型是基本类型,那么就只能通过在目标类型中定义以源类型为单参的构造函数实现类型转换。
    如果目标类型是基本类型,源类型是类类型,那么就只能通过在源类型中定义以目标类型为函数名的类型转换操作符函数实现类型转换。
    如果目标类型和源类型都是类类型,那么以上两种方法任取其一,但是不能同时使用。
    如果目标类型和源类型都是基本类型,那么无法实现自定义类型转换。

例:

#include <iostream>
using namespace std;
class Square {
public:
	double operator() (double x) {
		return x * x;
	}
};
class Integer {
public:
	explicit Integer (int i = 0) : m_i (i) {} //explicit强制要求用这个构造函数进行类型转换时必须要用显式类型转换
	
        
    void print (void) const {
		cout << m_i << endl;
	}
	Integer& operator() (int i) {
		m_i += i;
		return *this;//返回自引
	}

	Integer& operator, (int i) {
		m_i += i;
		return *this;
	}

	operator int (void) const {    //可以将Integer转换为int
		return m_i;
	}
private:
	int m_i;
};
void foo (const Integer& i) {
	i.print ();
}
Integer bar (void) {
	return Integer (300);//因为返回的是Integer,所以将300转换为Integer类型的
}
int main (void) {
	Square square;
	cout << square (3) << endl;//这个类的对象可以直接当函数使用。输出为“9”
//	cout << square.operator() (3) << endl;
	Integer i (10);
	i (1) (2) (3) (17);//10+1+2+3+17
	i.print (); // 33
	i, 1, 2, 3, 17;    //33+1+2+3+17
	i.print (); // 56
	i = (Integer)100;//i是Integer类型的,所以要将100进行转换
	i.print ();
	foo (static_cast<Integer> (200));//静态类型转换
	bar ().print ();
	int n = i;
	cout << n << endl;
	return 0;
}

5.new/delete操作符

例:

#include <iostream>
#include <cstdlib>
using namespace std;
class A {
public:
	~A (void) {}//析构函数

	static void* operator new (size_t size) {    // size_t等于unsigned int,但是一般要用size_t。    
		void* p = malloc (size);  //这是调用构造函数,创建对象
		cout << "我的new   :" << p << ' '
			<< size << endl;
		return p;                 //完成内存分配,返回分配好的内存地址
	}
	static void operator delete (void* p) {
		cout << "我的delete:" << p << endl;
		free (p);                 //完成内存释放
	}
	static void* operator new[] (size_t size) {
		void* p = malloc (size);  //这时调用构造函数,创建对象
		cout << "我的new[]   :" << p << ' '<< size << endl;
      //打印出分配的内存地址
		return p;                 //完成内存分配,返回分配好的内存地址
	}
	static void operator delete[] (void* p) {
		cout << "我的delete[]:" << p << endl;
		free (p);    //完成内存释放
	}
private:
	int m_i;
	double m_d;
	char m_c;
};	// IIIIDDDDDDDDCXXX  sizeof(A)为16
int main (void) {
	cout << sizeof (A) << endl;//16
	A* pa = new A;     //我的new:0x85bd008  16
	cout << pa << endl;//0x86bd008
	delete pa;         //我的delete:0x85bd008
	pa = new A[2];     //我的new[]:0x9fc2008  32
	cout << pa << endl;//0x8fc2008
	delete[] pa;       //我的delete[]:0x9fc2008
   //delete pa        //会崩溃,错误,因为delete的地址与new[]的地址不相同,会偏移四个字节,造成局部释放
        return 0;
}

五、关于操作符重载的限制

1.至少有一个操作数是类类型的。

int a = 10, b = 20;
int c = a + b; // 200
int operator+ (int a, int b) {
  return a * b;
} // ERROR !

2.不是所有的操作符都能重载。

  1. :: - 作用域限定
  2. . - 直接成员访问
  3. .* - 直接成员指针解引用
  4. ?: - 三目运算符
  5. sizeof - 获取字节数
  6. typeid - 获取类型信息

3.不是所有的操作符都可以用全局函数的方式实现。

= - 拷贝赋值
[] - 下标
() - 函数
-> - 间接成员访问

第四章 继承与多态

一、继承的基本概念

继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承(例如儿子继承父亲财产)类似。
继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类B继承于类A,那么B就拥有A的成员变量和成员函数。被继承的类称为父类或基类,继承的类称为子类或派生类。

二、继承的语法
class 子类名 : 继承方式1 基类1, 继承方式2 基类2, … {

};
继承方式:
公有继承 - public - 最常用方式
私有继承 - private - 缺省方式
保护继承 - protected - 特殊的私有继承

三、公有继承
1.通过继承,在基类中定义的任何成员,也都成为了子类的成员,但是基类的私有成员,子类虽然拥有却不能直接访问。
2.基类中的保护成员,可以被子类直接访问,但不能在无关的类和全局域中被访问。
3.任何一个子类对象中都包含着它的基类子对象。如果在子类的构造函数中没有明确指明其基类子对象如何被构造,系统将采用无参的方式构造该子对象。如果在初始化表中指明了基类子对象的构造方式,就调用相应的构造函数构造该子对象。
4.子类对象的构造和析构顺序
按照继承表的顺序依次构造每个基类子对象->按照声明的顺序依次构造每个成员变量->执行子类构造函数体中的代码
析构的过程与构造严格相反
5.一个子类对象在任何都可以被视为它的基类对象——IsA。
任何时候,一个子类对象的指针或者引用,都可以被隐式地转换为它的基类类型的指针或者引用,但是反过来,将基类类型的指针或者引用转换为它的子类类型必须显示地(static_cast)完成。
Student s (…);
Human* h = &s; // OK !
6.在子类中定义的任何和基类成员同名的标识符,都可以将基类中的该成员隐藏起来。通过作用域限定操作符“::”,可对该成员解隐藏。

四、继承方式对访控属性的影响

class A {
X:
   void foo (void) { ... }
};
class B : Y A {
  void bar (void) {
    foo (); // 仅需考虑X
  }
};
int main (void) {
  B b (...);
  b.foo(); // 不仅要考虑X还要考虑Y
}
class C : Z B {
  void fun (void) {
    foo(); // 不仅要考虑X还要考虑Y
  }
};

当通过一个子类对象(B)访问它从基类(A)中继承过来的成员(foo)的时候,需要考虑子类(B)从基类(A)继承时所采用的继承方式。

1.访控属性

关键字 属性 基类 子类 外部 友员
public 公有 OK OK OK OK
protected 保护 OK OK NO OK
private 私有 OK NO NO OK

2.基类的成员被继承到子类中以后,其访控属性会因不同的继承方式而异。

基类 公有继承 保护继承 私有继承
公有 公有 保护 私有
保护 保护 保护 私有
私有 私有 私有 私有

五、子类的构造函数和析构函数

1.子类隐式地调用基类的构造函数
在子类的构造函数中没有显示地指明其基类部分如何构造,隐式地调用基类的无参构造函数。如果子类没有定义任何构造函数,其缺省无参构造函数同样会隐式地调用基类的无参构造函数。

2.子类显式地调用基类的构造函数
在子类构造函数的初始化表中指明其基类部分的构造方式。

class A {
public:
  A (void) : m_data (0) {}
  A (int data) : m_data (data) {}
private:
  int m_data;
};
class B : public A {
public:
  B (int data) : A (data) {}
};
class A { ... };
class B : public A { ... };
class C : public B { ... };
C c (...);
构造:A->B->C
析构:C->B->A3.继承链的构造和初始化顺序

注:任何时候子类中基类子对象的构造都要先于子类构造函数中的代码。

六、子类的拷贝构造和拷贝赋值

子类的缺省拷贝构造和拷贝赋值除了复制子类的特有部分以外,还会复制其基类部分。如果需要自己定义子类的拷贝构造和拷贝赋值,一定不要忘记在复制子类特有部分的同时,也要复制其基类部分,否则将无法得到完整意义上的对象副本。

七、私有继承和保护继承

用于防止或者限制基类中的公有接口被从子类中扩散。
class DCT {
public:
  void codec (void) { ... }
};
class Jpeg : protected DCT { //只有自己类内可以用
public:
  void render (void) {
    codec (...);
  }
};
Jpeg jpeg;
jpeg.codec (...); // ERROR !防止公有接口被从子类中扩散

Jpeg Has A DCT,实现继承

class Jpeg2000 : public Jpeg {
public:
  void render (void) {
    codec (...); // OK ,code在Jpeg类中是保护型的,而通过公有继承,可以访问
  }
};

例:

#include <iostream>
using namespace std;
class Human {
public:
	Human (const string& name, int age) :
		m_name (name), m_age (age) {} //构造函数
	void who (void) const {
		cout << m_name << "," << m_age << endl;
	}
	void eat (const string& food) const {
		cout << "我在吃" << food << endl;
	}
protected:
	string m_name;
	int m_age;
};


class Student : public Human {
public:
	Student (const string& name, int age, int no) :
		Human (name, age), m_no (no) {} //正确的构造函数创建Student类的对象的同时先创建基类Huamn类,所以会调用Human类的构造函数,这里指定Human所调用的构造函数与所写的构造函数相匹配,所以不会出错。
	/*
  Student (const string& name, int age, int no) :
		 m_no (no) {
      m_name=name;
      m_age=age;
  }
  */ 错误的构造函数会报错,原因是这里调用Human的构造函数时没有指定方式,默认使用无参构造,但是在基类Human类中没有无参构造,所以会报错



	Student (const Student& that) :
		Human (that), m_no (that.m_no) {} //拷贝构造,显式的指明了调用基类的拷贝构造函数
	
Student& operator= (const Student& that) { //操作符重载
		if (&that != this) {
			Human::operator= (that);//显式的调用基类的拷贝赋值
			m_no = that.m_no;
		}
		return *this;
	}
	void learn (const string& lesson) const {
		cout << "我(" << m_name << "," << m_age
			<< "," << m_no << ")在学" << lesson
			<< endl;
	}
	using Human::eat;//如果没有这句,则Human的eat与这里的eat作用域不再一起,则下面的eat不会与Human中的eat构成重载,而是构成隐藏关系
                    //但是有了这句之后,将Human的eat在这里可见,既作用域也被声明在这里,则两个eat构成了重载关系
	
    void eat (void) const {
		cout << "我绝食!" << endl;
	}
//	int eat;
private:
	int m_no;
};
int main (void) {
	Student s1 ("张飞", 25, 1001);
	s1.who ();
	s1.eat ("包子");
	s1.learn ("C++");
	Human* h1 = &s1;//子类的指针可以隐式转换为基类的指针,因为访问范围缩小了,是安全的
	h1 -> who ();
	h1 -> eat ("KFC");
//	h1 -> learn ("C");//基类的指针或对象不可以访问子类中的成员
  Student* ps = static_cast<Student*> (h1);//基类的指针不可以隐式的转换为子类的指针,因为访问范围扩大,不安全,所以必须显式的进行转换,但是这样有风险
	ps -> learn ("C");
	Student s2 = s1;
	s2.who (); //子类的指针或对象可以方位基类的成员
	s2.learn ("英语");
	Student s3 ("赵云", 20, 1002);
	s3 = s2;
	s3.who ();
	s3.learn ("数学");
	return 0;  
}

用于防止或者限制基类中的公有接口被从子类中扩散。

class DCT {
public:
  void codec (void) { ... }
};
class Jpeg : protected DCT {
public:
  void render (void) {
    codec (...);
  }
};
Jpeg jpeg;
jpeg.codec (...); // ERROR !
//Jpeg Has A DCT,实现继承
class Jpeg2000 : public Jpeg {
public:
  void render (void) {
    codec (...); // OK !
  }
};

八、多重继承

多继承是指一个子类继承多个父类。多继承对父类的个数没有限制,继承方式可以是公共继承、保护继承和私有继承,不写继承方式,默认是private继承。

例:

#include <iostream>
using namespace std;
class Phone {                    //基类1
public:
	Phone (const string& numb) : m_numb (numb) {}
	void call (const string& numb) {
		cout << m_numb << "致电" << numb << endl;
	}
	void foo (void) {
		cout << "Phone::foo" << endl;
	}
private:
	string m_numb;
};

class Player {                 //基类2
public:
	Player (const string& media) : m_media (media){}
	void play (const string& clip) {
		cout << m_media << "播放器播放" << clip
			<< endl;
	}
	void foo (int data) {
		cout << "Player::foo" << endl;
	}
private:
	string m_media;
};

class Computer {          //基类3
public:
	Computer (const string& os) : m_os (os) {}
	void run (const string& prog) {
		cout << "在" << m_os << "上运行" << prog
			<< endl;
	}
private:
	string m_os;
};

class SmartPhone : public Phone, public Player,
	public Computer {          //多重继承
   public:
	SmartPhone (const string& numb,
		const string& media, const string& os) :
		Phone (numb), Player (media),
		Computer (os) {}
	using Phone::foo;  //将Phone中的foo函数的作用域声明到这里
	using Player::foo; //将Player中的foo函数的作用域声明到这里
   //这样就构成了重载
};
int main (void) {
	SmartPhone sp ("13910110072", "MP3", "Android");
	sp.call ("01062332018");
	sp.play ("High歌");
	sp.run ("愤怒的小鸟");
	Phone* p1 = reinterpret_cast<Phone*> (&sp);
	Player* p2 = reinterpret_cast<Player*> (&sp);
	Computer* p3 = reinterpret_cast<Computer*>(&sp);
	cout << &sp << ' '<< p1 << ' ' << p2 << ' '
		<< p3 << endl;  //地址都相同,但如果不用reinterpret的话,用隐式或者静态转换,p1 p2 p3将sp地址段一分为三,所以p1 p2 p3 地址会不同
	sp.foo ();
	sp.foo (100);
	return 0;
}
  • 虚继承
    在继承表中通过virtual关键字指定从公共基类中虚继承,这样就可以保证在最终子类对象中,仅存在一份公共基类子对象的实例,避免沿着不同的继承路径访问公共基类子对象中的成员时,所引发的数据不一致的问题。只有当所创建对象的类型回溯(su)中存在钻石结构时,虚继承才起作用,否则编译器会直接忽略virtual关键字。
#include <iostream>
using namespace std;
class A {    //公共基类
public:
	A (int i) : m_i (i) {}
protected:
	int m_i;
};
class B : virtual public A {
public:
	B (int i) : A (i) {}
	void set (int i) {
		m_i = i;
	}
};
class C : virtual public A { //virtual是虚继承
public:
	C (int i) : A (i) {}
	int get (void) {
		return m_i;
	}
};
class D : public B, public C {
public:
	D (int i) : B (i), C (i), A (i) {}//真正起作用的是A(i),不用B(i),C(i),但要写
};
int main (void) {
	D d (1000);//B里的m_i与C里的m_i都存的是2000
	cout << d.get () << endl; // 1000,调用C中的get,返回C中的m_i的值
	d.set (2000);//调用B类中的set,给B中的m_i赋值
	cout << d.get () << endl; // 输出为2000,---如果B,C没有virtual调用C中的get,D的初始化表中没有A(i),返回C中的m_i的值,则会输出1000.----因为有了,所以制定从公共基类中虚继承,所以在最终子对象中只有一份公共基类子对象的实例
//B b(3000);  //B创建的对象没有钻石结构,所以写了virtual也不起作用,依然拥有A的基类子对象
	return 0;
}

九、虚函数与多态

1.定义:
如果将基类中的一个成员函数声明为虚函数,那么子类中的同型函数就也成为虚函数,并且对基类版本形成覆盖。这时,通过一个指向子类对象的基类指针,或者一个引用子类对象的基类引用,调用该虚函数时,实际被调用的函数不由该指针或引用的类型决定,而由它们的目标对象决定,最终导致子类中覆盖版本被执行。这种现象称为多态。

例:

#include <iostream>
using namespace std;
class Shape {
public:
	Shape (int x, int y) : m_x (x), m_y (y) {}
	virtual void draw (void) {
		cout << "形状(" << m_x << ',' << m_y << ')'
			<< endl;
	}
protected:
	int m_x, m_y;
};
class Rect : public Shape {    //矩形
public:
	Rect (int x, int y, int w, int h) :
		Shape (x, y), m_w (w), m_h (h) {}
	void draw (void) {    //隐藏shape中的draw,构成隐藏关系
		cout << "矩形(" << m_x << ',' << m_y << ','
			<< m_w << ',' << m_h << ')' << endl;
	}
private:
	int m_w, m_h;
};
class Circle : public Shape {  //圆形
public:
	Circle (int x, int y, int r) :
		Shape (x, y), m_r (r) {}
	void draw (void) {
		cout << "圆形(" << m_x << ',' << m_y << ','
			<< m_r << ')' << endl;
	}
private:
	int m_r;
};
void render (Shape* shapes[]) {
	for (size_t i = 0; shapes[i]; ++i)//挨个解析
		shapes[i]->draw ();}
//因为在shape中的draw()有virtual修饰为虚函数,而另外两个子类中的同名draw也变为虚函数,覆盖了基类shape中的draw
  且调用时,有指针的指向的目标类型决定执行哪一个函数,真正执行的是覆盖版本的draw
  所以就通过指针调用各自的draw(),这样就可以用各自的绘制方法画出图形;(有了virtual修饰,则是按照指针指向的对象来找draw)
   但是如果没有在基类shape中的draw()没有用virtual修饰,则shape类型的指针会访问shape类中的draw,则全部会用基类shape中的draw绘制图形(是根据指针类型来找draw)

int main (void) {
	Shape* shapes[1024] = {};  //定义的基类类型的指针数组,这样就可以指向不同子类类型的对象
	shapes[0] = new Rect (1, 2, 3, 4);
	shapes[1] = new Circle (5, 6, 7);
	shapes[2] = new Circle (8, 9, 10);
	shapes[3] = new Rect (11, 12, 13, 14);
	shapes[4] = new Rect (15, 16, 17, 18);
	render (shapes);
	return 0;
}

十、函数覆盖的条件

  • overload - 重载
  • override - 覆盖、重写、改写

1.基类版本必须是虚函数。
2.函数名、形参表和常属性必须严格一致。
3.如果返回基本类型或者对象,那么也必须严格一致。如果返回类类型的指针或引用,那么子类版本也可以返回基类版本的子类。

class B : public A { ... };
基类:virtual A* foo (void) {...}
子类:A* foo (void) { ... }
      B* foo (void) { ... }

4.子类的覆盖版本不能比基类版本声明更多的异常抛出。
5.子类覆盖版本的访控属性与基类无关。

class A {
public:
  virtual void foo (void) { ... }
};
class B : public A {
private:
  void foo (void) { ... }
};
int main (void) {
  B* b = new B;
  b->foo (); // ERROR !foo在B中是私有的
  A* a = new B;
  a->foo (); // OK ! -> B::foo  访控属性是看指针类型的,在A中,foo 是公共部分的,所以可以访问,但真正执行的是覆盖版本的B中的foo。
}

十一、多态=虚函数+指针/引用

Shape shape = rect;//shape只能代表shape代表不了rect。
shape->draw ();    // Shape::draw
Shape& shape = rect;
shape->draw ();    // Rect::draw
-------------------------------------------------
class A {
public:
  A (void) {
    bar (); // A::bar   //构造函数中调用虚函数,永远没有多态型,构造A的时候,B还没有构造好,没法调用B中的尚未构造好的覆盖版本。
  }
  ~A (void) {
    bar (); // A::bar  //析构函数中调用虚函数,永远没有多态性,因为析构的顺序和构造相反,当执行基类中的析构函数时,子类已经析构后释放完了,无法调用析构后的覆盖版本
  }
  void foo (void) {
    This->bar (); // B::bar
  }
  virtual void bar (void) {
    cout << 'A' << endl;
  }
};
class B : public A {
  void bar (void) {
    cout << 'B' << endl;
  }
};
int main (void) {
  B b; // A
  b.foo (); // B   因为foo函数是A类中的成员函数,所以this指针是A类型的,这个this指针指向B类型的对象b,调用那个虚函数的覆盖版本看指针指向的目标对象,所以调用B中的bar
  return 0;
}

十二、纯虚函数、抽象类、纯抽象类

  • virtrual 返回类型 成员函数名 (形参表) = 0;的虚函数被称为纯虚函数。
  • 一个包含了纯虚函数类称为抽象类,抽象类不能实例化为对象。
  • 如果一个类继承自抽象类,但是并没有为其抽象基类中的全部纯虚函数提供覆盖,那么该子类就也是一个抽象类。
class A { // 纯抽象类
  virtual void foo (void) = 0;
  virtual void bar (void) = 0;
  virtual void fun (void) = 0;
};
class B : public A { // 抽象类
  void foo (void) { ... }
};
class C : public B { // 抽象类
  void bar (void) { ... }
};
class D : public C { // 具体类
  void fun (void) { ... }
};

除了构造和析构函数以外,所有的成员函数都是纯虚函数的类称为纯抽象类。

例:

#include <iostream>
using namespace std;
class Shape {
public:
	Shape (int x, int y) : m_x (x), m_y (y) {}
	virtual void draw (void) = 0;  //空函数,纯虚函数,因为有了这个纯虚函数,所以shape为抽象类。所以shape不能实例化,不能创建对象,如果该类中除了构造和析构函数之外,都是纯虚函数,则该类为纯抽象类,同样不能实例化。
protected:
	int m_x, m_y;
};
class Rect : public Shape {
public:
	Rect (int x, int y, int w, int h) :
		Shape (x, y), m_w (w), m_h (h) {}
	void draw (void) {
		cout << "矩形(" << m_x << ',' << m_y << ','
			<< m_w << ',' << m_h << ')' << endl;
	}
//      int draw (void){}         //不构成任何合法关系
//	int draw (void) const {}  //会隐藏,因为形参不同(这里的this是const类型)
//	int draw (int){}          //隐藏
private:
	int m_w, m_h;
};
class Circle : public Shape {
public:
	Circle (int x, int y, int r) :
		Shape (x, y), m_r (r) {}
	void draw (void) {
		cout << "圆形(" << m_x << ',' << m_y << ','
			<< m_r << ')' << endl;
	}
private:
	int m_r;
};
void render (Shape* shapes[]) {
	for (size_t i = 0; shapes[i]; ++i)
		shapes[i]->draw ();
}
int main (void) {
	Shape* shapes[1024] = {};
	shapes[0] = new Rect (1, 2, 3, 4);
	shapes[1] = new Circle (5, 6, 7);
	shapes[2] = new Circle (8, 9, 10);
	shapes[3] = new Rect (11, 12, 13, 14);
	shapes[4] = new Rect (15, 16, 17, 18);
	render (shapes);
//	Shape shape (1, 2);
	return 0;
}

十三、动态绑定(后期绑定、运行时绑定)

1.虚函数表

class A {
public:
  virtual void foo (void) { ... }
  virtual void bar (void) { ... }
};
class B : public A {
public:
  void foo (void) { ... }
};
A* pa = new A;
pa->foo (); // A::foo
pa->bar (); // A::bar
---------------------
A* pa = new B;
pa->foo (); // B::foo
pa->bar (); // A::bar

2.动态绑定

当编译器看到通过指向子类对象的基类指针或者引用子类对象的基类引用,调用基类中的虚函数时,并不急于生成函数调用代码,相反会在该函数调用出生成若干条指令,这些指令在程序的运行阶段被执行,完成如下动作:

  • 根据指针或引用的目标对象找到相应虚函数表的指针;
  • 根据虚函数表指针,找到虚函数的地址;
  • 动态绑定对程序的性能会造成不利影响。如果不需要实现多态就不要使用虚函数。

例:

#include <iostream>
using namespace std;
class A {
public:
	virtual void foo (void) {
		cout << "A::foo()" << endl;
	}
	virtual void bar (void) {
		cout << "A::bar()" << endl;
	}
};
class B : public A {
public:
	void foo (void) {            //覆盖
		cout << "B::foo()" << endl;
	}
};
int main (void) {
	A a;   //a
	void (**vft) (void) = *(void (***) (void))&a;  //使vft(二级指针)指向a的虚函数表,因为a是A类型的,所以强制转换为void(***),虚函数表是一个函数指针数组,要指向他,应该用指向指针的指针--二级指针。
	cout << (void*)vft[0] << ' '
		<< (void*)vft[1] << endl; //A类中foo函数与bar函数的地址
	vft[0] ();//调用了A类的foo
	vft[1] ();//调用了A类的bar

	B b;
	vft = *(void (***) (void))&b;//使vft指向B类的虚函数表
	cout << (void*)vft[0] << ' '
		<< (void*)vft[1] << endl;//B类中的foo函数与A类中的bar函数地址
	vft[0] ();//调用了B类的foo函数
	vft[1] ();//调用了A类的bar函数
	return 0;
}

标题

1.typeid操作符

例:

#include <iostream>
#include <typeinfo>
#include <cstring>
using namespace std;
class A {
public:
	virtual void foo (void) {}
};
class B : public A {};
void print (A* pa) {
    //	if (! strcmp (typeid (*pa).name (), "1A"))
	if (typeid (*pa) == typeid (A))
		cout << "pa指向A对象!" << endl;
	else
    //	if (! strcmp (typeid (*pa).name (), "1B"))
	if (typeid (*pa) == typeid (B))
		cout << "pa指向B对象!" << endl;
}
int main (void) {
	cout << typeid (int).name () << endl;           //'i'
	cout << typeid (unsigned int).name () << endl;  //'j'
	cout << typeid (double[10]).name () << endl;    //A10_d
	cout << typeid (char[3][4][5]).name () << endl; //A3_A4_A5_c
	char* (*p[5]) (int*, short*);                   //函数指针数组
	cout << typeid (p).name () << endl;             //A5_PFPcPiPsE
	cout << typeid (const char* const* const).name (//
		) << endl;     //PKPKc  指针指向一个常量,这个常量是个指针,这个指针指向一个常量,这个常量是char类型的。
	cout << typeid (A).name () << endl;//1A
	A* pa = new B;
	cout << typeid (*pa).name () << endl;//A中有虚函数,所以'1B’,如果A中没有虚函数,则是‘1A’。没有多态,则会按照指针本身的类型,有多态则会按照指针指向的目标对象类型。
	print (new A);//“pa指向A对象”
	print (new B);//“pa指向B对象”
}

2. dynamic_cast

例:

t << "-------- dc --------" << endl;
                  //动态类型转换,运行期间检查。
	// A是B的基类,pa指向B对象,成功
	B* pb = dynamic_cast<B*> (pa);
	cout << pb << endl;    //地址
	// A不是C的基类,pa没有指向C对象,失败,安全
	C* pc = dynamic_cast<C*> (pa);
	cout << pc << endl;    // 0
	A& ra = b;     //引用子类对象的基类引用。
	try {
		C& rc = dynamic_cast<C&> (ra);
	}
	catch (exception& ex) {
		cout << "类型转换失败:" << ex.what ()
			<< endl;
		// ...
	}
	// pa没有指向D对象,失败,安全
	D* pd = dynamic_cast<D*> (pa);
	cout << pd << endl;
	
        cout << "-------- sc --------" << endl;
	       //静态类型转换,编译期间检查。
           // B是A的子类,成功
	pb = static_cast<B*> (pa);
	cout << pb << endl;
	// C是A的孙子类,成功,危险!
	pc = static_cast<C*> (pa);
	cout << pc << endl;

	 // D不是A的后裔,失败,安全
     //	pd = static_cast<D*> (pa);   //两个方向都不能做隐式转换,所以任何方向也不能做静态转换。
     //	cout << pd << endl;
	
        cout << "-------- rc --------" << endl;
	          //重解释类型转换
        // 无论在编译期还是在运行期都不做检查,危险!
	pb = reinterpret_cast<B*> (pa);
	cout << pb << endl;
	pc = reinterpret_cast<C*> (pa);
	cout << pc << endl;
	pd = reinterpret_cast<D*> (pa);   cout << pd << endl;   return 0;  }

十五、虚析构函数

将基类的析构函数声明为虚函数,delete一个指向子类对象的基类指针,实际被执行的将是子类的析构函数,而子类的析构函数可以自动调用基类的析构函数,进而保证子类特有的资源,和基类子对象中的资源都能够得到释放,防止内存泄漏。如果基类中存在虚函数,那么就有必要为其定义一个虚析构函数,即使该函数什么也不做。

例:

#include <iostream>
using namespace std;
class A {
public:
	A (void) {
		cout << "A构造" << endl;
	}
	virtual ~A (void) {     //析构函数只有一个,声明为virtual则会形成覆盖,这个叫做虚析构函数
		cout << "A析构" << endl;
	}
};
class B : public A {
public:
	B (void) {
		cout << "B构造" << endl;
	}
	~B (void) {
		cout << "B析构" << endl;
	}
};
int main (void) {
       B* pb=new B;     //先调用A再调用B的构造
       delete pb;       //调用B的析构,再自动调用A的析构

	   A* pa = new B;   //指向子类的基类指针,先调用A的构造,再调用B的构造
	   delete pa;       //因为基类中的析构函数是虚函数,调用B的析构,B的析构自动调用A的构析。如果基类中的析构函数没有被声明为虚函数,则会调用基类的析构,不会调用子类的析构,会出现泄露
	return 0;
}


第五章 指针与引用


引用

  1. 定义:为对象起一个别名。必须初始化,程序会把引用和它的初始值绑定在一起,此后无法再令引用重新绑定到两外一个对象,因此引用必须初始化。

对const的引用:即常说的常量引用,对常量的引用不能被用作修改它所绑定的对象。

const int ci = 1024;
const int &r1 = ci; // 正确,引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:试图让一个非常量引用指向一个常量对象。

对const的引用可能引用一个并非const的对象:常量引用只是对引用可参与的操作做出了限定,对引用的对象本身是不是一个常量并未限定。

指针

  1. 空指针:不指向任何对象,如下方法用来初始化一个指针:
int *p1 = nullptr;
int *p2 = 0;
int *p3 = NULL;
  1. void* 指针:一种特殊的指针类型,用于存放任意对象的地址。利用它能做的事儿比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋值给另外一个void* 指针。不能直接操作void*指针所指的对象,因为我们并不知道这个对象是什么类型,也就无法确定能在这个对象上做哪些操作。
  2. 指向常量的指针:和常量引用一样,指向常量的指针也没有规定其所指对象必须是一个常量。仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
  3. 指向常量的指针:不能改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。
const double pi = 3.14; // pi是个常量,它的值不能改变
double *ptr = &pi; // 错误:ptr是一个普通指针
const double *cptr = &pi; // 正确:cptr可以指向一个双精度常量
*cptr = 42; // 错误:不能给*cptr赋值
  1. const指针:必须初始化,且之后不能改变。把*放在const前面来说明指针是一个常量。这个含义是说不变的是指针本身的值,而非指向的那个值。
int errNumb = 0;
int *const curErr = &errNumb; // currErr将一直指向errNumb
const double pi = 3.14; 
const double *const pip = &pi; // pip是一个指向常量对象的常量指针
  1. 顶层const与底层const:指针本身是一个对象,它可以指向另外一个对象。因此指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。所以有了顶层const和底层const的概念。

注:顶层const:表示指针本身是一个常量 ;底层const:表示指针所指的对象是一个常量



第六章 异常和I/O流

一、为什么要有异常——WHY?

1.通过返回值表达错误
局部对象都能正确的析构 ; 层层判断返回值,流程繁琐

例:

#include <iostream>  
#include <cstdio>
using namespace std;
int func3 (void) {
	FILE* fp = fopen ("none", "r");//fopen失败会返回控指针NULL。
	if (! fp)
		return -1;
	// ...
	fclose (fp);
	return 0;
}
int func2 (void) {
	if (func3 () == -1)
		return -1;
	// ...
	return 0;
}
int func1 (void) {
	if (func2 () == -1)
		return -1;
	// ...
	return 0;
}
int main (void) {
    //层层判断返回值
	if (func1 () == -1) {    
		cout << "执行失败!改天再见!" << endl;
		return -1;
	}
	// ...
	cout << "执行成功!恭喜恭喜!" << endl;
	return 0;
}

2.通过setjmp/longjmp远程跳转

一步到位进入错误处理,流程简单 ; 局部对象会失去被析构的机会。

例:

#include
#include
#include //标c的函数,跳转
using namespace std;
jmp_buf g_env; //jmp是专门为c量身定造的,有类的情况不适用,会跳转,因为不执行右括号,局部对象失去执行析构的机会,不会调用析构函数,会造成内存泄露
class A {
public:
A (void) {
cout << “A构造” << endl;
}
~A (void) {
cout << “A析构” << endl;
}
};
void func3 (void) {
A a;
FILE* fp = fopen (“none”, “r”);
if (! fp)
longjmp (g_env, -1); //(没有定义类的时候)这个时候是的g_env变为-1,但是不在这返回,在main函数的setjmp处返回
// …
fclose (fp);
}
void func2 (void) {
A a;
func3 ();
// …
}
void func1 (void) {
A a;
func2 ();
// …
}
int main (void) {
if (setjmp (g_env) == -1) { //(没有定义类的时候)第一次到这,genv是0,所以执行下面的func1(),执行了后在fun3中的longjmp处在缓冲区使得g_env变为1,并在这使g_env返回
cout << “执行失败!改天再见!” << endl;
return -1;
}
func1 ();
// …
cout << “执行成功!恭喜恭喜!” << endl;
return 0;
}


3.异常处理
局部对象都能正确的析构 一步到位进入错误处理,流程简单


二、异常的语法——WHAT?

1.异常的抛出

  • throw 异常对象;
  • 异常对象可以是基本类型的变量,也可以是类类型的对象。
  • 当程序执行错误分支时抛出异常。

2.异常的捕获

try {
  可能抛出异常的语句块;
}
catch (异常类型1 异常对象1) {
  处理异常类型1的语句块;
}
catch (异常类型2 异常对象2) {
  处理异常类型2的语句块;
}
...
catch (...) {
  处理其它类型异常的语句块;
}

三、异常处理的使用方法——HOW?

1.抛出基本类型的异常,用不同的值代表不同的错误。
例:

#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
void foo (void) {
	FILE* fp = fopen ("none", "r");
	if (! fp)
		throw "打开文件失败!";
	void* pv = malloc (0xFFFFFFFF);
	if (! pv)
		throw "内存分配失败!";
	// ...
}
int main (void) {
	try {
		foo ();                //可能引发异常的语句快
	}
	catch (const char* ex) {   //char类型的异常对象类型
		cout << ex << endl;
		return -1;   //
	}       //有none文件,显示内存分配失败,没有none文件,显示打开文件失败
	return 0;
}

2.抛出类类型的异常,用不同的类型表示不同的错误。

例:

#include <iostream>/
#include <cstdio>
#include <cstdlib>
using namespace std;
class Error {};
class FileError : public Error {};
class MemError : public Error {};
void foo (void) {
	FILE* fp = fopen ("none", "r");
	if (! fp)
		throw FileError ();   //放到安全区
	void* pv = malloc (0xFFFFFFFF);
	if (! pv)
		throw MemError ();
	// ...
}
int main (void) {
	try {
		foo ();
	}
	catch (FileError& ex) {         //用引用,效率高,避免拷贝构造  
		cout << "打开文件失败!" << endl;
		return -1;
	}
	catch (MemError& ex) {//用引用
		cout << "内存分配失败!" << endl;
		return -1;
	}
	catch (Error& ex) {             //如果放在最前面:会有警告,会捕获所有异常,一个子类的对象可以用基类的引用去引用,虽然后面有更适合的匹配,但是最先匹配原则
		cout << "一般性错误!" << endl;
		return -1;
	}
	return 0;
}

3.通过类类型的异常携带更多诊断信息。

例:

#include <iostream>
#include <cstdio>
#include <cstdlib>
using namespace std;
class Error {
public:
	virtual void print (void) const = 0;
};
class FileError : public Error {
public:
	FileError (const string& file, int line) :
		m_file (file), m_line (line) {}
	void print (void) const {
		cout << "在" << m_file << "文件的第"
			<< m_line << "行,发生了文件错误!"
			<< endl;
	}
private:
	string m_file;
	int m_line;
};
class MemError : public Error {
public:
	void print (void) const {
		cout << "内存不够啦!!!" << endl;
	}
};
void foo (void) {
	FILE* fp = fopen ("none", "r");
	if (! fp)
		throw FileError (__FILE__, __LINE__);  //这里抛出,所以不执行下面的语句,所以下面的异常没有抛出
	void* pv = malloc (0xFFFFFFFF);
	if (! pv)
		throw MemError ();
	// ...
}
int main (void) {
	try {
		foo ();
	}
	catch (Error& ex) {
		ex.print ();
		return -1;
	}
	return 0;
}

4.忽略异常和继续抛出异常。

例:

#include <iostream>
//继续抛出  记住用引用
using namespace std;
void foo (void) {
	throw 10;
}
void bar (void) {
	try {
		foo ();
	}
	catch (int& ex) {
		--ex;  //安全区中的ex变为9
		throw; // 继续抛出 抛出的ex为9.
	}
	// ...
}
int main (void) {
	try {
		bar ();
	}
	catch (int& ex) {
		cout << ex << endl; // 9
	}
	return 0;
}

5.异常说明

  • 在一个函数的形参表后面写如下语句:
    …形参表) throw (异常类型1, 异常类型2, …) { … } 表示这个函数可以被捕获的异常。
  • throw () - 这个函数所抛出的任何异常均无法捕获。
  • 没有异常说明 - 这个函数所抛出的任何异常均可捕获。
class A {
  virtual void foo (void)
    throw (int, double) { ... }
  virtual void bar (void)
    throw () { ... }
};
class B : public A {
  void foo (void)
    throw (int, char) { ... }  // ERROR  不能比基类抛出更多的异常
  
void bar (void) { ... }        // ERROR  不可以这样写覆盖函数
  void bar (void)
    throw () { ... }
};

例:

#include <iostream>
using namespace std;
void foo (void) throw (int, double, const char*){ //可以被捕获到的类型
//	throw 1;
//	throw 3.14;
	throw "Hello, Exception !";
}
int main (void) {
	try {
		foo ();
	}
	catch (int ex) {            //可以捕获int的异常
		cout << ex << endl;
	}
	catch (double ex) {         //可以捕获double的异常
		cout << ex << endl;
	}
	catch (const char* ex) {    //可捕获const char*的异常
		cout << ex << endl;
 //               cout<<__LINE__<<endl;
	}
	return 0;
}

6.使用标准异常

#include <stdexcept>

四、构造函数中的异常

  • 构造函数可以抛出异常,而且有些时候还必须抛出异常,已通知调用者构造过程中所发生的错误。
  • 如果在一个对象的构造过程中抛出了异常,那么这个对象就称为不完整对象。不完整对象的析构函数永远不会被指向,因此需要在throw之前,手动释放动态的资源。

例:

#include <iostream>
#include <stdexcept>
#include <cstdio>
//构造函数中的异常
using namespace std;
class FileError : public exception {
private:
	const char* what (void) const throw () {  //
		return "文件访问失败!";
	}
};
class B {
public:
	B (void) {
		cout << "B构造" << endl;
	}
	~B (void) {
		cout << "B析构" << endl;
	}
};
class C {
public:
	C (void) {
		cout << "C构造" << endl;
	}
	~C (void) {
		cout << "C析构" << endl;
	}
};
class A : public C {
public:
	A (void) : m_b (new B) {
		FILE* fp = fopen ("none", "r");
		if (! fp) {
			delete m_b;           //再抛出异常前,将资源释放,因为一旦抛出异常,就不会执行析构函数。
			throw FileError ();   //会有回滚机制,所有调用的构造函数,会反向执行一遍析构函数,但除了动态变
		}
		// ...
		fclose (fp);
	}
	~A (void) {
		delete m_b;
	}
private:
	B* m_b;
//	C m_c;
};
int main (void) {
	try {
		A a; //执行A的拷贝构造,
		// ...
	}
	catch (exception& ex) {
		cout << ex.what () << endl;//执行覆盖版本
		return -1;
	}
	return 0;
}

五、析构函数中的异常

永远不要在析构函数中抛出异常。

class A {
public:
  ~A (void) {
    //throw -1;  //错误,抛出后,直接到析构函数最后的花括号(1),然后直接到main函数中的花括号(2),花括号(2)结束,又调用此析构函数,形成死循环
    try {

      sysfunc ();
    }
    catch (...) {}
  }(1)
};
try {
  A a;
  a.foo ();
}  (2)
catch (...) { ... }

通过try-catch拦截所有可能引发的异常。

六、C++的I/O流库

  • C:fopen/fclose/fread/fwrite/fprintf/fscanf/fseek/ftell
  • C++:对基本的I/O操作做了类的封装,其功能没有任何差别,用法和C的I/O流也非常近似。

七、格式化I/O

<< / >>

例:

#include <iostream>
#include <fstream>
//格式化I/O
using namespace std;
int main (void) {
        //格式化的写
	ofstream ofs ("format.txt");    //相当与c中的w,新加的内容会覆盖原有内容,打开这个文件,打开失败,ofs为false,成功为true
	if (! ofs) {
		perror ("打开文件失败");
		return -1;
	}
	ofs << 1234 << ' ' << 56.78 << ' ' << "tarena"
		<< '\n';     //将这写写入到文件里
	ofs.close ();    //关闭文件,如果不写也可以,结束后,调用ofstream析构函数会关闭掉
	
        ofs.open ("format.txt", ios::app);    //相当于c中的以a方式打开,可以在文件中追加,不会覆盖原有的内容
	if (! ofs) {
		perror ("打开文件失败");
		return -1;
	}
	ofs << "append_a_line\n";
	ofs.close ();

        //格式化的读
	ifstream ifs ("format.txt");    //要求文件必须存在,否则报错
	if (! ifs) {
		perror ("打开文件失败");
		return -1;
	}
	int i;
	double d;
	string s1, s2;
	ifs >> i >> d >> s1 >> s2;    //读取文件中的内容到程序中。
	cout << i << ' ' << d << ' ' << s1 << ' '
		<< s2 << endl;
	ifs.close ();     //关闭文件
	return 0;
}

八、非格式化I/O

put / get

例:

#include <iostream>
#include <fstream>
//非格式化I/O
using namespace std;
int main (void) {
	ofstream ofs ("putget.txt");         //定义一个ofstream类
	if (! ofs) {
		perror ("打开文件失败");
		return -1;
	}
	for (char c = ' '; c <= '~'; ++c)    //前加加返回的是一个引用,效率高,后加加返回的是拷贝,要进行一次拷贝,所以效率低。
		if (! ofs.put (c)) {      //ofs.put成功返回true,否则false。向文件中写入。
			perror ("写入文件失败");
			return -1;
		}
	ofs.close ();

	ifstream ifs ("putget.txt");
	if (! ifs) {
		perror ("打开文件失败");
		return -1;
	}
	char c;
	while ((c = ifs.get ()) != EOF)     //读如字符,直到返回EOF(表示读取完毕)。
		cout << c;
	cout << endl;
	if (! ifs.eof ()) {       //或者if(ifs.error())都可以判断是否出错
		perror ("读取文件失败");
		return -1;
	}
	ifs.close ();
	return 0;
}

九、随机I/O

seekp / seekg    (p->put,g->get)
tellp / tellg

例:

#include <iostream>
#include <fstream>
//随机I/O
using namespace std;
int main (void) {
	fstream fs ("seek.txt", ios::in | ios::out);      //即可读也可写,相当于c中的r+,要求文件必须存在。
	if (! fs) {
		perror ("打开文件失败");      //打印错误信息。最近的一次错误的原因。
		return -1;
	}
	fs << "0123456789";  //向文件里输入。
	cout << fs.tellp () << endl;    //获取写指针的位置,在下一个接受数据的位置,最后一个9所在的位置是9,所以写的位置为10
	cout << fs.tellg () << endl;    //获取读指针的位置,虽然没有读,但是会随着写指针一起走
 
        //seekp()与seekg()函数分别有两个参数,第一个是偏移量,正负代表前后方向,第二个是从哪个位置开始
	fs.seekp (-3, ios::cur);   //调整写指针的位置,表示从当前位置往文件头移动三个字符
	fs << "XYZ";    //覆盖789
	fs.seekg (4, ios::beg);    //调整读指针的位置,表示从文件头开始偏移四个位置
	int i;
	fs >> i;      //从4开始读,读到6,后面是xyz所以不读,结束。
	cout << i << endl;
	cout << fs.tellg () << endl;   //7
	cout << fs.tellp () << endl;   //7
	
        fs.seekg (-6, ios::end);     //从文件尾开始向文件头偏移6个位置
	fs << "ABC";
	fs.close ();
	return 0;
}

十、二进制I/O

read / write
K 0 - 255
A^K=B
B^K=A
PKI
HAS MD5
//后续会更新详解,这里简单介绍一下吧

例:(异或机制)

#include <iostream>
#include <fstream>
#include <stdexcept>
#include <cstdlib>
using namespace std;
#define BUFSIZE (1024*10)
int _xor (const char* src, const char* dst,
	unsigned char key) {    //源文件,目标文件,密钥
	ifstream ifs (src, ios::binary);   //以二进制方式读
	if (! ifs) {
		perror ("打开源文件失败");
		return -1;
	}
	ofstream ofs (dst, ios::binary);
	if (! ofs) {
		perror ("打开目标文件失败");
		return -1;
	}

	char* buf = NULL;   //创建缓冲区
	try {
		buf = new char[BUFSIZE];
	}
	catch (bad_alloc& ex) {      //bad_alloc是标准库里的
		cout << ex.what () << endl;
		return -1;
	}
	while (ifs.read (buf, BUFSIZE)) {    //缓冲区的地址,和大小
		for (size_t i = 0; i < BUFSIZE; ++i)
			buf[i] ^= key;      //将每一个字符都与key异或
		if (! ofs.write (buf, BUFSIZE)) {   //以二进制方式写进去,缓冲区地址,和希望写的大小
			perror ("写入文件失败");
			return -1;
		}
	}
	if (! ifs.eof ()) {   //判断是否正常
		perror ("读取文件失败");
		return -1;
	}

	for (size_t i = 0; i < ifs.gcount (); ++i)   //gcount函数返回剩下的大小
		buf[i] ^= key;     //将缓冲区中的剩下的也与key异或
	if (! ofs.write (buf, ifs.gcount ())) {
		perror ("写入文件失败");
		return -1;
	}
	delete[] buf;  //释放缓冲区
	ofs.close ();  //关闭文件
	ifs.close ();  //关闭文件
	return 0;
}
int enc (const char* plain, const char* cipher) {
	srand (time (NULL));
	unsigned char key = rand () % 256;  //0到256sui随机数
	if (_xor (plain, cipher, key) == -1)  //为-1则失败
		return -1;
	cout << "密钥:" << (unsigned int)key << endl; //告诉密钥是什么。转换为数的形式
	return 0;
}
int dec (const char* cipher, const char* plain,
	unsigned char key) {
	return _xor (cipher, plain, key);
}
int main (int argc, char* argv[]) {
	if (argc < 3) {
		cerr << "用法:" << argv[0]
			<< " <明文文件> <密文文件>" << endl;
		cerr << "用法:" << argv[0]
			<< " <密文文件> <明文文件> <密钥>"
			<< endl;
		return -1;
	}
	if (argc < 4)
		return enc (argv[1], argv[2]);
	else
		return dec (argv[1], argv[2],
			atoi (argv[3]));      return 0;
    }

十一、格式控制

C++为标准输入和输出定义了一些格式标志, 它可以通过flags(), setf(), 和 unsetf() 三个函数来控制.

  • boolalpha 可以使用单词"true"和"false"进行输入/输出的布尔值.
  • dec 用十进制格式显示后面的数值.
  • fixed 用正常的记数方法显示浮点数(与科学计数法相对应).
  • hex 用十六进制格式显示后面的数值.
  • internal 将填充字符回到符号和数值之间.
  • left 输出调整为左对齐.
  • oct 用八进制格式显示后面的数值.
  • right 输出调整为右对齐.
  • scientific 用科学记数法显示浮点数.
  • showbase 输出时显示所有数值的基数.
  • showpoint 显示小数点和额外的零,即使不需要.
  • showpos 在非负数值前面显示"+".
  • skipws 当从一个流进行读取时,跳过空白字符(spaces, tabs, newlines).
  • unitbuf 在每次插入以后,清空缓冲区.
  • uppercase 以大写的形式显示科学记数法中的"e"和十六进制格式的"x".

例:

#include <iostream>
#include <iomanip>   //要加这个头文件
#include <cmath>     //数学库
#include <fstream>   //文件头文件
#include <sstream>   //字符串流
//格式控制
using namespace std;
int main (void) {
	cout << sqrt (2) << endl;   //求平方根,只输出六位有效数字,1.41421
	cout.precision (10);        //将精度设为10,10位有效数字
	cout << sqrt (2) << endl;   //输出十位。1.414213562
	cout << sqrt (2) * 100 << endl;   //141.4213562
	cout << setprecision (5) << sqrt (2) << endl   //将精度改为5,1.4152
		<< sqrt (2) * 100 << endl;   //141.42
	cout << "当前精度:" << cout.precision ()      //可以返回当前精度
		<< endl;
	cout << setprecision (2) << 1.24 << ' ' << 1.25
		<< ' ' << 1.26 << endl;        //将精度设为2,1.2 1.2 1.3,只有大于5才会入,小于等于5都舍去。
	cout << showbase << hex << 127 << endl;   //打印进制标志,hex为十六进制 
	cout << oct << 127 << endl;   //oct为8进制
	cout << dec << 127 << endl;   //dec为10进制
	cout << noshowbase << hex << 127 << dec << endl;   //关闭现实进制,并恢复为十进制,一次性起作用,输出紧跟着的后一个输出,后面的不会管。
	cout << setw (12) << 127 << 721 << endl;   //设置域宽,默认靠右,填充空格,只对127起作用,对721不起作用,一次性。
	cout << setfill ('$') << left << setw (12)    //用$填充,左对齐,域宽为12
		<< 127 << endl;
	cout.precision (10);    //精度修改为10,对科学计数法和定点形式意义不一样
	cout.setf (ios::scientific);  //以科学计数法输出
	cout << sqrt (2) << endl;   //1.4142135624e+00//小数部位为十位
	cout.setf (ios::fixed);    //一定点小数形式输出(正常的)
	cout << sqrt (2) << endl;    //1.414213562 有效数字为十位
	cout << 12.00 << endl;     //12.00000000  因为前面精度设为10
	cout << showpoint << 12.00 << endl;    //显示小数点
	cout << noshowpoint << 12.00 << endl;   //不显示小数点
	
        ifstream ifs ("stream.txt");  //打开文件
	ifs.unsetf (ios::skipws);   //取消跳过空白,否则默认将空格等制表符认为是分隔作用,不读取
	char c;
	while (ifs >> c)
		cout << c;
	ifs.setf (ios::skipws);   //又设置回跳过空白
	ifs.clear ();     // 复位,将流复位,状态恢复到文件头,否则只改位置指针没有用
	ifs.seekg (ios::beg);   //此时位置指针不再文件头,这可以将位置指针返回文件头
	while (ifs >> c)
		cout << c;
	ifs.close ();
	cout << endl;

	int i = 1234;
	double d = 56.78;
	string s = "tarena";
	ostringstream oss;     //定义输出字符串流对象
	oss << i << ' ' << d << ' ' << s;
	string str = oss.str ();  //将字符串流流中的内容拿出来
	cout << str << endl;   //1234 56.78 tarena
	str = "hello 3.14 pai";
	istringstream iss;  //定义输入字符串流对象
	iss.str (str);      //将str的内容读入字符串流
	iss >> s >> d >> str;
	cout << s << ' ' << d << ' ' << str << endl;
	return 0;
}

十二、字符串流

class Student {
  ...
private:
  string m_name;
  int m_age;
};
Student s ("张三", 25);
ofs.write (&s, sizeof (s));  //这样只是写入了地址,不能直接用二进制的方式写,

例:

#include <iostream>
#include <fstream>
#include <cstring>
//类中有字符串,如何写入文件
using namespace std;
class Dog {
public:
	Dog (const string& name = "", int age = 0) :
		m_age (age) {
		strcpy (m_name, name.c_str ());
	}
	void print (void) const {
		cout << m_name << "," << m_age << endl;
	}
private:
	char m_name[128];
	int m_age;
};
int main (void) {
	ofstream ofs ("dog.dat");
	Dog dog ("小白", 25);
	ofs.write ((char*)&dog, sizeof (dog));   //写进去的只是地址,指针
	ofs.close ();
	ifstream ifs ("dog.dat");
	Dog dog2;
	ifs.read ((char*)&dog2, sizeof (dog2));
	dog2.print ();
	ifs.close ();
	return 0;
}

? ? ? ? 本片文章献给初学萌新,大佬绕过!!!!
? ? ? ? 仅以基础学习,望各位萌新可以从本文获得收获!!!!
? ? ? ? 本文代码有引用网络上常见板子,整理不易 勿喷!!!!

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值