C++STL的基本学习(一)

前言

钻研一下C++的STL,这一片还不细学STL,先熟悉一下C++的模板、读写、异常等概念

 

 

 

 

 

 

 


函数模板:

基本语法

看一段c++代码,实现简单的数据交换。

#include <iostream>
using namespace std;

//int 类型数据交换
void MySwap(int& a,int& b) {
	int temp = a;
	a = b;
	b = temp;
}

int main()
{
	int a, b;
	cin >> a >> b;

	MySwap(a, b);
	cout << a << b<<endl;
	return 0;
}

我们来引入模板技术

template<typename T> 
void MySwap(T& a,T& b) {
	int temp = a;
	a = b;
	b = temp;
}
  • template<typename T>等价于template<class T>
  • 其中的参数T可以随便起名,另外可以设置多个参数

调用的时候可以直接调用或者显式指定类型

	MySwap(a, b);
	MySwap<int>(a, b);

 

函数模板调用规划

函数模板和普通函数的区别:

  • 函数模板不允许自动类型转化
  • 普通函数能够自动进行类型转化:对传入参数与形参不一样,会自动装换传入参数为形式参数

函数模板与普通函数一起调用规则:

  • 函数模板可以像普通函数那样重载
  • C++编译器优先考虑普通函数
  • 如果函数模板可以产生一个更好的匹配,那么选择模板
  • 可以通过空模板实参列表的语法(<>)限定编译器只能通过模板匹配

 

编译原理

cpp编译原理

  1. hello.c经过预处理器,将宏展开,生成的文件hello.i
  2. hello.i经过编译器,将文件编译成汇编语言,生成文件为hello.s
  3. hello.s经过汇编器,将文件编译成目标文件hello.o(win下为hello.obj)
  4. hello.o经过链接器,将文件编译成可执行文件

 

compile和link是大多数语言从原代码生成可执行程序的两个步骤。

之所有有这两个步骤因为几乎任何一个程序都不是用一个原636f70793231313335323631343130323136353331333262383033文件写出来的。compile是先针对单独原文件进行处理。link是把compile处理的结果组合成一个完整的可执行文件。

其实C/C++完全也可以一步成型,不需要compile和link两个步骤,但是那样的后果就是:一,每次生成可执行程序,必须翻译全部源代码;二,C语言的执行库(printf, scanf这些)必须都以源代码形式存在。这怎么样也说不过去吧。

另外头文件不属于compile和link过程,头文件是预编译过程的文件。

C/C++语言的完整编译过程是

一、预编译

处理#define #if #include这类#开头的语句,这些称为预编译指令。这个过程中会把.h文件和.c/.cpp文件组合成最终交给compile过程的原文件。这个原文件是不包含任何#开头的语句的。所有#define定义的宏也会被替换。

二、编译
把上面那个原文件编译成.o或者VC里是.obj文件。这个文件保存了机器码化的函数、函数的描述、全局变量的描述、乃至段的描述等等。

三、连接
把可执行程序需要的所有的编译过程产生的.o或者.obj文件组合到一起。(这里也包括.lib文件,.lib文件件本质上就是打包的.obj文件集合)。另外连接过程还会组合一些其他数据,比如资源、可执行文件头等等。

 

编译示例

我们来直接示例一下

我这里通过openssh将cmd控制台连接到了一个远程Linux服务器上,

安装g++编译工具:

 yum -y install gcc+ gcc-c++

GCC(GNU Compiler Collection)是Linux下最主要的编译工具,GCC不仅功能非常强大,结构也异常灵活。它可以通过不同的前端模块来支持各种语言,如Java、Fortran、Pascal、Modula-3和Ada

g++是GCC中的一个工具,专门来编译C++语言的。

关于g++的使用建议参考文章

新建一个cpp文件

接下来,我们开始预编译

-E  让GCC在预处理结束后停止编译  

我们可以通过cat查看一下index.i的文件

我们可以看到,只有最下面才是我们写的程序,之前的#include<iostream>以及#define MAX 1024全部消失不见,上面有了一大串程序。

  • 其实上面的内容就是iostream头文件的内容,预编译将这个头文件的内容全部拷贝了过来。
  • define之所以消失是因为预编译将它实现了,它将你的代码中的所有MAX都替换成了1024。

接下来 我们生成汇编文件

g++ -S index.i -o index.s

打开index.s

这就是大名鼎鼎的汇编语言

接下来我们生成目标文件:

g++ -C index.s -o index.o

index.o的文件内容不是人看的,我就没必要截图了。

Obj就是目标文件,是你的源程序经过编译程序编译后生成的,它不能直接执行,需要连接程序连接后才能生成可执行文件,这样就能值行了。这种目标文件一般是由机器代码组成的,但也有例外,可以是自己定义的一些伪指令代码,但这样还需有专门的解释程序对其进行解释执行,连接程序是把目标代码和它所使用的库文件连接的程序。

最后我们来进行链接:

g++ index.s -o index

生成index文件就可以执行了。

 

模板函数实现原理

模板函数并不是定义出来直接就可以用的。

它的原理是生成一个模板函数,再去调用这个模板函数。

比如我们MyAdd(float,float)会生成一个模板函数,这个模板函数是float类型MyAdd(int ,int ) 会生成一个模板函数,是int类型。

每当有调用运行,都会根据函数模板生成一个函数,然后调用这个函数。之后如果再有同类型的函数,就会直接调用先前生成的这个函数

函数模板机制结论:

  1. 编译器并不是把函数模板处理成能够处理任何类型的函数
  2. 函数模板通过具体类型产生不同的函数
  3. 编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译

函数模板应用示例

这里我们来写个冒泡排序算法,熟悉一下函数模板

#include <iostream>
using namespace std;

template<typename T>
void  PrintArray(T* arr, int len) {
	for (int i = 0; i < len; i++) {
		cout << arr[i]<<" ";
	}
	cout << endl;
}
template<typename T>
void MySwap(T& a, T& b) {
	//骚气的交换方法
	a = a + b;
	b = a - b;
	a = a - b;
}

//排序算法(冒泡排序-降序)
template<typename T>
void MySort(T* arr, int len) {
	for (int i = 1; i < len; i++) {
		for (int j = 0; j < len-i; j++) {
			if (arr[j] < arr[j + 1])
				MySwap(arr[j], arr[j + 1]);
		}
	}
}
int main()
{
	int arr[] = { 1,2,3,5,1,2,4,4,6,2 };
	int len = sizeof(arr) / sizeof(int);
	PrintArray(arr, len);
	MySort(arr, len);
	PrintArray(arr, len);

	char chArr[] = { 'f','q','w','e','d','f','a' };
	int chLen = sizeof(chArr) / sizeof(char);
	PrintArray(chArr, chLen);
	MySort(chArr, chLen);
	PrintArray(chArr, chLen);

	return 0;
}

 


类模板

基本使用

类模板的语法和函数模板其实比较类似。

但是:函数模板在调用的时候,可以自动类型推导,但是类模板必须显式指定类型。

我们在原来的“解决方案”下面新建一个项目,并且右击设为启动项目。

template<typename T>
class Person {
public :
	Person(T id,T age) {
		this->mId = id;
		this->mAge = age;
	}
	void Show() {
		cout << "ID:" << mId << " Age:"
			 << mAge << endl;
	}
private:
	T mId;
	T mAge;
};

int main()
{
	Person<int> p(10,20);
	p.Show();

	return 0;
}

这就是类模板的基本语法。

类模板派生普通类

如果我们需要给类模板创造子类,那应该怎么写呢?

//类去定义对象,对象需要编译器分配内存
//所以继承父类需要指定类型
class SubPerson :public Person<int> {


};

指定清楚类型即可

类模板派生类模板

template<typename T>
class Teacher:public Person<T> {
	
};

继续使用T来代替

 

类模板h和cpp分离编写

C++基础我们可以知道.h文件和.cpp文件是这么处理的:

Person.h

#pragma once

#include
using namespace std;

class Person {
public :
	Person(int age);
	void Show();
private:
	int age;
};

Person.cpp

#include"Person.h"

Person::Person(int age) {
	this->age = age;
}

void Person::Show() {
	cout << "Age:" << age << endl;
}

main.cpp

#include <iostream>
#include "Person.h"

using namespace std;

int main()
{
	Person person(12);
	person.Show();
	
	return 0;
}

这样就简单实现了“类声明”与“类实现”的分离,由此类推,是不是类模板就是在此基础上加上template<typename T>即可?

答案并不是这样,如果你尝试一下,你就会发现vs报了这样的错误——无法解析的外部符号 “public: __thiscall Person<int>::Person<int>(int)” (??0?$Person@H@@QAE@H@Z)……”

为什么会这样呢?

这和模板实现机制,C++编译机制有关。

C++编译时是独立编译,即我编译一个文件的时候,不知道其他文件中有没有调用自己文件的函数。

编译器编译上面的main.cpp文件时,编译到Person<int> person(12) 语句时,它发现构造函数在当前文件没有找到,编译器会将函数位置先生成一个符号,编译认为这个函数在其他文件,于是让链接器在链接的时候去找这个函数的具体位置。

然后编译器编译Person.cpp,这里面的内容是函数模板,将会经过两次编译,经过第一次编译后,还并没有生成具体的函数 ,这个时候去链接main.cpp这个文件就会找不到对应的函数。

所以会出现报错。

如果不是.h和.cpp分离的话,main.cpp直接include了Person.h,Person.h中有函数的定义,所以就不会出现没有找到对应函数,先用符号代替的情况,所以就直接调用了,函数模板二次编译被直接调用就没有问题。

解决方法:在main.cpp中,修改#include “Person.h”为#include “Person.cpp”,这样预编译之后就会直接将Person.cpp内容拷贝进来,就不会出现找不到的情况了,Person.cpp也引入了Person.h,不必担心找不到类的情况。

另外,一般这种分开写的情况,会将“负责实现”的文件——Person.cpp 改为 Person.hpp,告诉别人这是一个类模板的实现文件。

 

类模板中的static关键字

类模板中定义的static属性,在一个指明类型的模板类出现之后,这个模板类才会有属于自己的这个static属性。

 


MyArray框架

新建一个项目,我们来编写一个属于自己的MyArray框架

#include <iostream>

using namespace std;

template<typename T>
class MyArray {
public:
	MyArray(int capacity) {
		this->mCapacity = capacity;
		this->mSize = 0;
		//申请内存空间
		this->pAddr = new T[this->mCapacity];
	}
	//拷贝构造(深拷贝,不与参数arr指向同一内存空间)
	MyArray(const MyArray& arr) {
		this->mSize = arr.mSize;
		this->mCapacity = arr.mCapacity;

		//申请内存空间
		this->pAddr = new T[this->mCapacity];
		for (int i = 0; i < this->mSize; i++) {
			this->pAddr[i] = arr.pAddr[i];
		}
	}
	T& operator[](int index) {
		return this->pAddr[index];
	}
	MyArray<T> operator=(const MyArray<T>& arr) {
		if (this->pAddr != NULL) {
			delete[] this->pAddr;
		}
		this->mSize = arr.mSize;
		this->mCapacity = arr.mCapacity;
		//申请内存空间
		this->pAddr = new T[this->mCapacity];
		for (int i = 0; i < this->mSize; i++) {
			this->pAddr[i] = arr.pAddr[i];
		}
		return *this;
	}

	void PushBack(T& data) {
		//判断容器中是否有位置(是否还有容量)
		if (this->mSize>=this->mCapacity) {
			return;
		}
                //调用拷贝构造 =操作符
                //对象元素必须能够被拷贝
                //考虑好对象是深拷贝还是浅拷贝
                //容器都是值寓意的,而非引用寓意
		this->pAddr[this->mSize] = data;
		this->mSize++;
	}
	~MyArray() {
		if (this->pAddr != NULL)
			delete[] this->pAddr;
	}

public:
	//一共可以容下多少个元素
	int mCapacity;
	//当前数组有多少元素
	int mSize;
	//保存数据的首地址
	T* pAddr;
};


void test01() {
	MyArray<int> mArray(20);
	int a = 10,b=20,c=30,d=40;
	mArray.PushBack(a);
	mArray.PushBack(b);
	mArray.PushBack(c);
	mArray.PushBack(d);

	for (int i = 0; i < mArray.mSize; i++) {
		cout << mArray[i] << "  ";
	}
	cout << endl;
}

int main()
{
	test01();

	return 0;
}

其中我们会发现,如果直接 mArray.PushBack(20) 就会报错,因为 20 是一个右值(可以理解为临时变量),对右值取引用后,就拿到了它的地址,对它的地址进行更改显然是不合理的。

解决方法:

  • void PushBack(T&& data),对右值取引用,就可以使用,内部实现不变,这是C++11的新标准
  • 形参指明const,要求不能改变

 

这样,我们的MyArray框架就基本实现了。

 

深拷贝与浅拷贝:

学C++包括很多语言都应该清楚一个概念:深拷贝与浅拷贝

是针对指针的:

浅拷贝是只拷贝指针地址,意思是浅拷贝指针都指向同一个内存空间,当原指针地址所指空间被释放,那么浅拷贝的指针全部失效。

深拷贝是先申请一块跟被拷贝数据一样大的内存空间,把数据复制过去。这样拷贝多少次,就有多少个不同的内存空间,干扰不到对方。


类型转换

基本语法

类型转换的含义是通过改变一个变量的类型为别的类型从而改变该变量的表示方式。为了类型转换一个简单对象为另一个对象你会使用传统的类型转换操作符。

C风格的强制类型转换,不管什么是什么类型,统统都是Type b=(Type) a。

C++风格的类型转换提供了4种类型转换操作符来应对不同场合的使用:

static_cast 一般的转换
dynamic_cast 通常在基类和派生类之间转换时使用
const_cast 主要针对const的转换
reinterpret_cast 用于进行没有任何关联之间的转换,比如一个

字符指针转换成一个整型数

static_cast

static_cast 用于内置的数据类型,还有具有继承关系的指针或者引用。

void test01() {
	int a = 97;
	char c = static_cast<char>(a);
	//输出了97对应的ascii码——a
	cout << c;
}

 

dynamic_cast

dynamic_cast只能转换具有继承关系的指针或者引用,只能由派生类型转成基类型。

为什么不能让基类指针转成派生类指针?

一般认为子类对象大小>=父类对象大小。为什么?因为子类可以扩展父类,可以增加成员变量。如果一个子类增加了成员变量,那么它的对象的内存空间会大于父类对象。这时一个实际指向父类的指针,如果被强制转化为子类对象指针,当使用这个指针时可能会导致越界访问非法内存。相反,为何子类指针可以转换为父类指针?因为父类指针需要的,子类对象都有,不会出现非法内存访问。

这个和虚函数不一样:人家虚函数是,当父类中声明某个函数为百虚函数,并且子类得载了这个虚度函数以后,用父类对象的指问针可以调用子类的相应答函数,但前提是该指针指向的对版象是子类的对象,否则没权有意义。

另注意,实现多态也只是虚函数被重写,子类的新增属性父类指针依旧无法使用:

class A
{
public:
	virtual void print()
	{
		cout << "this line is from A" << endl;
	}
};
class B : public A
{
public:
	string b = "这是B的属性";
	void print()
	{
		cout << "this line is from B" << endl;
	}
};
void main()
{
	A *p = new B;
	p->print();
	//报错,classA找不到b属性
        p->b;
	delete p;
}

 

const_cast

const_cast转换可以增加或去除变量的const性(内部机制是通过再创建一个新的非const的变量接收)

 

 

reinterpret_cast

任何类型的指针都可以转换成其他类型的指针,包括函数指针。

比较暴力,转换容易不安全

//这里将函数指针定义成了类型
typedef void(*FUNC1)(int, int);
typedef int(*FUNC2)(int);

void f(int a,int b) {
	cout << a + b;
}
void test02() {
	//无关的指针类型进行转换
	Animal* a = NULL;
	Building* buil = reinterpret_cast<Building*>(a);
	//函数指针转换
	FUNC1 func1=f;
	FUNC2 func2 = reinterpret_cast<FUNC2>(func1);
}

 

关于

关于类型转换,程序员需要知道:

  • 程序员必须清楚的知道要转变的变量,转换前是什么类型,转换后是什么类型,以及转换的后果。
  • 一般情况下,不建议类型的转换,避免类型转换

 


异常

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。

异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。

基本使用

int divide(int x, int y) {
	if (y == 0) {
		//抛出异常
		throw y; 
	}
	return x / y;
}

void test02() {
	try {
		//尝试捕获异常
		divide(10, 0);
	}
	catch (int exception) {
		//异常根据类型匹配
		cout << "除数为零"<<exception;
	}
}

C++中必须在try中抛出异常才能捕获。

异常会逐层向上级抛出,存在 跨函数性,即如果再定义一个 callDivide函数,由这个函数来调用divide,callDivide放在test02的try当中(作为保护代码),那么异常也是可以正常捕捉的,抛出异常后会逐级向上抛出。

栈解旋(unwinding)

异常被抛出后,从进入try块起,到异常被抛掷前,这期间所有在栈上构造的对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding)

class Person {
public:
	Person(){
		cout << "对象构建\n";
	}
	~Person() {
		cout << "对象被销毁\n";
	}

};
 
int divide(int x,int y) {

	Person p1, p2;
	if (y == 0) {
		throw y;
	}
	return x / y;
}
void test() {
	try{
		divide(5,0);
	}
	catch (int e) {
		cout << "捕获异常";
	}
}
int main()
{
	test();
}

输出:

对象构建
对象构建
对象被销毁
对象被销毁
捕获异常

 

异常接口声明

  • 为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型,例如:void func1() throw(A,B,C); 这个函数func能够且只能抛出异常类型ABC及其子类型的异常。
  • 如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常,例如:void func()
  • 一个不抛任何类型异常的函数可声明为:void func() throw()
  • 如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexcepted 函数会被调用,该函数默认行为调用 terminate 函数中断程序。
//这个函数只能抛出int、float、char类型的异常
void func() throw(int, float, char*) {
	//抛出 char* 程序运行终止
	throw "abc";

}

//不能抛出任何异常
void func02() throw() {
	throw - 1;
}

//可以抛出任何类型的异常
void func03() {
	throw - 1;
}

int main()
{
	try {
		//func();
		func02();
		func03();
	}
	catch (char str) {
		cout << str << endl;
	}
	catch (int e) {
		cout << "异常!" << endl;
	}
	/*catch (...) {
		cout<<"未知类型异常";
	}*/
	return 0;
}

注意:上面的测试代码在vs环境下会有问题,因为vs:警告 C4290 忽略 C++ 异常规范,但指示函数不是 __declspec(nothrow)

建议Linux环境下测试。

catch (...) {
		cout<<"未知类型异常";
}

捕获任何类型的异常

异常类型和异常变量的生命周期

  • throw的异常是有类型的,可以是数字、字符串、类对象
  • catch需严格匹配类型
void func1() {
	throw 1;
}
void func2() {
	throw "exception";
}

class MyException {
public:
	MyException(const char* str) {
		error = new char[strlen(str) + 1];
		strcpy(error, str);
	}
	//拷贝构造
	MyException(const MyException& ex) {
		this->error = new char[strlen(ex.error) + 1];
		strcpy(this->error, ex.error);
	}
	MyException& operator=(const MyException& ex) {
		if (this->error != NULL)
		{
			delete[] this->error;
			this->error = NULL;
		}
		this->error = new char[strlen(ex.error) + 1];
		strcpy(this->error, ex.error);
	} 
	void what() {
		cout << "异常啊" << endl;
	}
	~MyException() {
		if (error != NULL)
		{
			delete[] error;
		}
	}
private:
	char* error;
};
void func3() {
	//创建匿名对象并抛出
	throw MyException("有意思");
} 

int main()
{
	try {
		func1();
	}
	catch (int e) {
		cout << e << endl;
	}
	try {
		func2();
	}
	catch (const char* e) {
		cout << e << endl;
	}
	try {
		func3();
	}
	catch (MyException e) {
		e.what();
	}
	return 0;
}

MyException类为什么需要些拷贝构造函数?

因为在抛出异常时匿名构造创建的MyException对象在复制给catch后面的MyException时,两个对象指向了同一个内存空间,前一个对象调用了析构函数释放了error的内存,后一个对象就失去了自身的数据,就会报错,所以我们用拷贝构造使他们指向不同的内存空间。

析构中的delete是delete掉建立在堆上的error,不是本身匿名对象,别弄混了!

 

catch中捕获类对象的生命周期(用普通对象去接)

异常对象catch处理完之后就析构

class MyException {
public:
	MyException(const char* str) {
		cout << "构造函数"<<endl;
	}
	//拷贝构造
	MyException(const MyException& ex) {
		cout << "拷贝构造"<<endl;
	}
	void what() {
		cout << "异常" << endl;
	}
	~MyException() {
		cout << "析构函数"<<endl;
	}
private:
	char* error;
};
void func3() {
	//创建匿名对象并抛出
	throw MyException("有意思");
} 

int main()
{
	try {
		func3();
	}
	catch (MyException e) {
		e.what();
	}
	return 0;
}

构造函数
拷贝构造
异常
析构函数
析构函数

 

catch中捕获类对象引用的生命周期(用引用对象去接)

引用不用调用拷贝构造创建新对象

class MyException {
public:
	MyException(const char* str) {
		cout << "构造函数"<<endl;
	}
	//拷贝构造
	MyException(const MyException& ex) {
		cout << "拷贝构造"<<endl;
	}
	void what() {
		cout << "异常" << endl;
	}
	~MyException() {
		cout << "析构函数"<<endl;
	}
private:
	char* error;
};
void func3() {
	//创建匿名对象并抛出
	throw MyException("有意思");
} 

int main()
{
	try {
		func3();
	}
	catch (MyException& e) {
		e.what();
	}
	return 0;
}

构造函数
异常
析构函数

 

catch中捕获类对象指针的生命周期(用指针对象去接)

class MyException {
public:
	MyException() {
		cout << "构造函数"<<endl;
	}
	//拷贝构造
	MyException(const MyException& ex) {
		cout << "拷贝构造"<<endl;
	}
	void what() {
		cout << "异常" << endl;
	}
	~MyException() {
		cout << "析构函数"<<endl;
	}
private:
	char* error;
};
void func3() {
	//创建匿名对象并抛出
	throw &MyException();
} 
int main()
{
	try {
		func3();
	}
	catch (MyException* e) {
		e->what();
	}
	return 0;
}

构造函数
析构函数
异常

注意这里为什么析构了还可以调用成员函数:

析构函数,释放的是对象的内存,不知指针变量自己的内存,释放后虽然指针变量还存在,但是此指针所指向的内存已经不存在了,此时的指针就成为”野指针“。但是释放后要是调用对象的成员函数还是可以的,因为成员函数只在程序结束的时候释放,只要此时程序还没有结束就可以使用成员函数。

 

标准异常类

C++标准库的异常类结构层次:

  1. 在上述继承体系中,每个类都有提供了构造函数,复制构造函数,和赋值操作符重载。
  2. logic_error类及其子类、runtime_error类及其子类,他们的构造函数是接受一个string类型的形式参数,用于异常信息的描述。
  3. 所有的异常类都有一个what()方法,返回const char* 类型(C风格字符串)的值,描述异常信息。

关于异常类的更多说明以及具体的异常类的对应的功能可以参考菜鸟教程

编写自己的异常类示例:

#include <iostream>
#include <exception>
using namespace std;
 
struct MyException : public exception
{
  const char * what () const throw ()
  {
    return "C++ Exception";
  }
};
 
int main()
{
  try
  {
    throw MyException();
  }
  catch(MyException& e)
  {
    std::cout << "MyException caught" << std::endl;
    std::cout << e.what() << std::endl;
  }
  catch(std::exception& e)
  {
    //其他的错误
  }
}

 

自己编写异常类解决问题的示例

//异常基类
class  BaseException  {
public:
	//纯虚函数(表明这是抽象类)
	virtual void what()=0;
	virtual ~BaseException() {}
};
class TargetSpaceNullException :public BaseException {
	virtual void what() {
		cout << "目标空间空\n";
	}
	//~TargetSpaceNullException() {}
};
class SourceSpaceNullException :public BaseException {
	virtual void what() {
		cout << "源空间空\n";
	}
	//~SourceSpaceNullException() {}
};


void copy_str(char* target,const char* source) {
		if (target == NULL) {
			throw TargetSpaceNullException();
		}
		if (source == NULL) {
			throw SourceSpaceNullException();
		}
		while (*source != '\0') {
			*target = *source;
			target++;
			source++;
		}
}
int main()
{
	const char* source = "abcdefg";
	char buf[40] = {0};
	try {
		copy_str(buf, source);
	}
	catch (BaseException& e) {
		e.what();
	}
	cout << buf << endl;

	return 0;
}

 


小插曲:vs查看内存

惭愧用vs这么久居然不会查看内存

示例代码:

int main() 
{
	int i=10;
	int j = 7;
	int *p = &i;
	
	return 0;
}

然后在return 0上面设置一个断点。

然后运行程序,他就会卡在断点的位置。

这个时候,在你的 调试=》窗口 下面就有一个 内存 ,选择一个

然后,复制断点处提供的地址,就可以看到内存信息了:

0x0a=10,没问题

 

 

 

 


输入输出与文件操作

输入输出

C++输入输出流

输入和输出都是相对程序而说的。

其中的cout与cin就是我们经常用的标准输出

我们在vs中可以查看源代码:

__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2_IMPORT istream cin, *_Ptr_cin;
__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2_IMPORT ostream cout, *_Ptr_cout;
__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2_IMPORT ostream cerr, *_Ptr_cerr;
__PURE_APPDOMAIN_GLOBAL extern _CRTDATA2_IMPORT ostream clog, *_Ptr_clog;

可见,cout、cin其实就是ostream类、istream类的两个对象而已,全局流对象,在cout被创建的时候,cout对象就自动与显示器关联了(关联好了目标)。

另外还有cerr、clog,意为标准错误和标准日志,默认也是输出数据到显示器。其中cerr没有缓冲区,clog有缓冲区

缓冲区

 

在程序和键盘和显示器之间,还存在着缓冲区的概念,输入和输出都是对缓冲区的作用,例如我们直接输入cout<<“Hello world”,并不是直接将“Hello world”输出到屏幕上,而是存放在了输出缓冲区中,当我们endl(endl=flush+”\n”)之后,输出缓冲区会被刷新,输出缓冲区的数据就显示到了屏幕上。当然,如果你用一些编译器(比如vs),可能人家有了优化可以直接刷新缓冲区,还有一种说法是系统比较闲的时候会自动刷新输出缓冲区。

标准输入流

标准输入流对象cin,重点掌握的函数:

  • cin.get():一次只能读取一个字符
  • cin.get(一个参数):读一个字符
void test01() {
	char ch;
	/*输入回车后数据就到了输入缓冲区中,
	cin.get()就开始从输入缓冲区读字符,然后输出*/ 
	//知道输入Ctrl+z(模拟文件结尾EOF)循环结束
	while ((ch=cin.get())!=EOF) {
		cout << ch<<endl;
	}
}

输入:asdfsadf

输出:asdfsadf(每两个字符中间有换行)

  • cin.get(两个参数):可以读字符串
  • cin.getline():读取一行数据
  • cin.ignore():忽略输入缓冲区的字符
void test02() {
	char ch;
	//从缓冲区读取数据(如果缓冲区没有,它会阻塞)
	cin.get(ch);
	cout << ch<<endl;
	cin.ignore(2);
	cin.get(ch);
	cout << ch << endl;
}

输入:adsf

输出:af

  • cin.peek():偷窥输入缓冲区的第一个字符(不会取走这个字符)
void test05() {
	cout << "请输入数组或者字符串:"<<endl;
	char ch;
	//偷窥输入缓冲区的第一个字符,不拿走
	ch=cin.peek();
	if (ch >= '0'&&ch <= '9') {
		int number;
		//缓冲区的数据读到number中
		cin >> number;
		cout << "您输入的是数字:"<<number<<endl;
	}
	else {
		char buf[256];
		//缓冲区的数据读到number中
		cin >> buf;
		cout << "您输入的是字符串:" << buf << endl;
	}
}

请输入数组或者字符串:
sdafsadfdsaf
您输入的是字符串:sdafsadfdsaf

  • cin.putback():将字符放还到缓冲区中
void test06() {
	cout << "请输入数组或者字符串:" << endl;
	char ch;
	cin.get(ch);
	if (ch >= '0'&&ch <= '9') {
		//ch放回缓冲区中
		cin.putback(ch);
		int number;
		//缓冲区的数据读到number中
		cin >> number;
		cout << "您输入的是数字:" << number << endl;
	}
	else {
		cin.putback(ch);

		char buf[256];
		//缓冲区的数据读到number中
		cin >> buf;
		cout << "您输入的是字符串:" << buf << endl;
	}
}

学几个单词:peek:窥看、偷窥;putback:放回

 

标准输出流

标准输出流常用函数:

  • cout.flush():刷新缓冲区
  • cout.put():向缓冲区写字符
	//输出一个字符(支持链式编程)
	cout.put('h').put('e').put('l') << endl;
  • cout.write():二进制流的输出
  • cout.width():输出流格式控制
  • cout.fill():填充width中的空余位置
void test() {
	cout.width(10);
	cout.fill('*');
	cout.setf(ios::left);
	cout << 9 << endl;
}

9*********

  • cout.setf(标记):
void test() {
	int number = 10;
	cout << number << endl;
	//卸载当前默认的十进制输出方式
	cout.unsetf(ios::dec);
	//设置当前输出方式为八进制
	cout.setf(ios::oct);
	//展示8进制的标志(前面有个0)
	cout.setf(ios::showbase);
	cout << number<<endl;
}

常见的设置格式状态的格式标志

 
格式标志 作用
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 每次输出之后刷新所有的流
ios::stdio 每次输出之后清除stdout, stderr

 

除了通过成员函数设置标记输出16进制以外,我们还可以通过控制符输出

void test() {
	int number = 10;
	cout << hex
		<< setiosflags(ios::showbase)
     	        << setw(15)
		<<setfill('-')
		<<setiosflags(ios::left)
		<< 25
		<<endl;
}

0x19———–

 

文件操作

概述和普通文件操作

学完标准输入输出,我们来学习一下文件输入输出

主要利用的就是fstream、ifstream、ofstream这三个类

void test01() {
	const char* fileName = 
		"F:\\vs的平时作品\\STL学习模板\\STL学习模板\\source.txt";
	//只读方式打开文本文件(通过有参构造)
	ifstream ism(fileName,ios::in);
	//这里底层重载了!,可以判断打开成功吗
	if (!ism) {
		cout << "打开文件失败!"<<endl;
		return;
	}
	//读文件
	char ch;
	while (ism.get(ch)) {
		cout << ch;
	}
	//关闭文件
	ism.close();
}

然后就实现了将文件数据读到程序中并打印到控制台。

如果要拷贝文件:

void test01() {
	const char* fileName = 
		"F:\\vs的平时作品\\STL学习模板\\STL学习模板\\source.txt";
	const char* fileNameDes =
		"F:\\vs的平时作品\\STL学习模板\\STL学习模板\\target.txt";
	//只读方式打开文本文件(通过有参构造)
	ifstream ism(fileName,ios::in);
	ofstream osm(fileNameDes, ios::out);
	//这里底层重载了!,可以判断打开成功吗
	if (!ism) {
		cout << "打开文件失败!"<<endl;
		return;
	}
	//读文件
	char ch;
	while (ism.get(ch)) {
		cout << ch;
		osm.put(ch);
	}
	//关闭文件
	ism.close();
	osm.close();
}

文件操作常用标志符:

ios::in

为输入(读)而打开文件

ios::out

为输出(写)而打开文件

ios::ate

初始位置:文件尾

ios::app

所有输出附加在文件末尾

ios::trunc

如果文件已存在则先删除该文件

ios::binary

二进制方式

二进制文件操作和对象序列化

序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

示例:

class Person {
public:
	Person() {}
	Person(int age,int id):age(age),id(id) {}
	void Show() {
		cout << "Age:" << age 
			<< "Id:" << id << endl;
	}
private:
	int age;
	int id;
};

void test01() {
	Person p1(10, 1), p2(14, 2);
	//把p1,p2写入文件中
	/*记住,对象是以二进制的方式在存储器中
	所以我们可以将对象以二进制的方式存入文件中(即序列化)*/
	const char* fileNameDes = 
		"F:\\vs的平时作品\\STL学习模板\\STL学习模板\\target.txt";
	ofstream osm(fileNameDes,ios::out|ios::binary);
	//二进制的方式写文件
	/*从p1的地址开始,读取Person类的大小的字节*/
	osm.write((char*)&p1,sizeof(Person));
	osm.write((char*)&p2, sizeof(Person));

	osm.close();

	ifstream ism(fileNameDes,ios::in|ios::binary);
	Person p_1,p_2;
	//从文件读取数据,
	ism.read((char*)&p_1, sizeof(Person));
	ism.read((char*)&p_2, sizeof(Person));
	p_1.Show();
	p_2.Show();

	ism.close();
}

我们发现对象成功还原了

这个过程就是对象的序列化。


 

商业转载 请联系作者获得授权,非商业转载 请标明出处,谢谢

 

 

发表评论