C++学习笔记(3) - 高级编程
1. 文件和流
接下来介绍如何从文件读取流和向文件写入流。需要用到 C++ 中标准库 fstream
要在 C++ 中进行文件处理,必须在 C++ 源代码文件中包含头文件 <iostream> 和 <fstream>
1.1 打开文件
//filename 指定要打开的文件的名称和位置,mode 定义文件被打开的模式
void open(const char *filename, ios::openmode mode);
模式标志 | 描述 |
---|---|
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
如果想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么可以使用下面的语法:
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
如果想要打开一个文件用于读写,可以使用下面的语法:
ifstream afile;
afile.open("file.dat", ios::out | ios::in );
1.2 关闭文件
当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。
但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。
void close();
1.3 写入文件
在 C++ 编程中,使用流插入运算符( << )向文件写入信息,就像使用该运算符输出信息到屏幕上一样。
唯一不同的是,在这里使用的是 ofstream 或 fstream 对象,而不是 cout 对象。
1.4 读取文件
在 C++ 编程中,使用流提取运算符( >> )从文件读取信息,就像使用该运算符从键盘输入信息一样。
唯一不同的是,在这里使用的是 ifstream 或 fstream 对象,而不是 cin 对象。
1.5 代码示例
以读写模式打开一个文件,在向文件 afile.dat 写入用户输入的信息之后,程序从文件读取信息,并将其输出到屏幕上
#include <fstream>
#include <iostream>
using namespace std;
int main ()
{
char data[100];
// 以写模式打开文件
ofstream outfile;
outfile.open("afile.dat");
cout << "Writing to the file" << endl;
cout << "Enter your name: ";
cin.getline(data, 100);
// 向文件写入用户输入的数据
outfile << data << endl;
cout << "Enter your age: ";
cin >> data;
cin.ignore();
// 再次向文件写入用户输入的数据
outfile << data << endl;
// 关闭打开的文件
outfile.close();
// 以读模式打开文件
ifstream infile;
infile.open("afile.dat");
cout << "Reading from the file" << endl;
infile >> data;
// 在屏幕上写入数据
cout << data << endl;
// 再次从文件读取数据,并显示它
infile >> data;
cout << data << endl;
// 关闭打开的文件
infile.close();
return 0;
}
当上面的代码被编译和执行时,它会产生下列输入和输出:
$./a.out
Writing to the file
Enter your name: Zara
Enter your age: 9
Reading from the file
Zara
9
1.6 文件位置指针
istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg(“seek get”)和关于 ostream 的 seekp(“seek put”)。
seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。
文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。下面是关于定位 “get” 文件位置指针的实例:
// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );
// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );
// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );
// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );
2. 异常处理
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。
- try: try 块中的代码标识将被**的特定异常。它后面通常跟着一个或多个 catch 块。
- catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
- throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
// try/catch 语句的语法
try
{
// 保护代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
2.1 抛出异常
以下是尝试除以零时抛出的异常
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
2.2 捕获异常
catch 块跟在 try 块后面,用于捕获异常。
可以指定想要捕捉的异常类型,这是由 catch 关键字后的括号内的异常声明决定的。
try
{
// 保护代码
}catch( ExceptionName e )
{
// 处理 ExceptionName 异常的代码
}
try
{
// 保护代码
}catch(...)
{
// 能处理任何异常的代码
}
2.3 代码示例
抛出一个除以零的异常,并在 catch 块中捕获该异常
#include <iostream>
using namespace std;
double division(int a, int b)
{
if( b == 0 )
{
throw "Division by zero condition!";
}
return (a/b);
}
int main ()
{
int x = 50;
int y = 0;
double z = 0;
try {
z = division(x, y);
cout << z << endl;
}catch (const char* msg) {
cerr << msg << endl;
}
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Division by zero condition!
2.4 定义新的异常
#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)
{
//其他的错误
}
}
产生以下结果:
MyException caught
C++ Exception
what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。
3. 动态内存
了解动态内存在 C++ 中是如何工作的是成为一名合格的 C++ 程序员必不可少的。C++ 程序中的内存分为两个部分:
- 栈:在函数内部声明的所有变量都将占用栈内存。
- 堆:这是程序中未使用的内存,在程序运行时可用于动态分配内存。
3.1 new 运算符
通用语法
new data-type;
data-type 可以是包括数组在内的任意内置的数据类型,也可以是包括类或结构在内的自定义的任何数据类型。
定义一个指向 double 类型的指针,然后请求内存,该内存在执行时被分配。
double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存
如果自由存储区已被用完,可能无法成功分配内存。
建议检查 new 运算符是否返回 NULL 指针,并采取以下适当的操作:
double* pvalue = NULL;
if( !(pvalue = new double ))
{
cout << "Error: out of memory." <<endl;
exit(1);
}
malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。
new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。
3.2 delete 运算符
当某个已经动态分配内存的变量不再需要使用时,使用 delete 操作符释放它所占用的内存
// 释放 pvalue 所指向的内存
delete pvalue;
3.3 代码示例
#include <iostream>
using namespace std;
int main ()
{
double* pvalue = NULL; // 初始化为 null 的指针
pvalue = new double; // 为变量请求内存
*pvalue = 29494.99; // 在分配的地址存储值
cout << "Value of pvalue : " << *pvalue << endl;
delete pvalue; // 释放内存
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Value of pvalue : 29495
3.4 数组的动态内存分配
这里直接用二维数组进行举例
int **array
// 假定数组第一维长度为 m, 第二维长度为 n
// 动态分配空间
array = new int *[m];
for( int i=0; i<m; i++ )
{
array[i] = new int [n] ;
}
//释放
for( int i=0; i<m; i++ )
{
delete [] arrar[i];
}
delete [] array;
3.5 对象的动态内存分配
使用一个对象数组来理清这一概念
#include <iostream>
using namespace std;
class Box
{
public:
Box() {
cout << "调用构造函数!" <<endl;
}
~Box() {
cout << "调用析构函数!" <<endl;
}
};
int main( )
{
Box* myBoxArray = new Box[4];
delete [] myBoxArray; // 删除数组
return 0;
}
通过之前学习的构造析构的知识,以上程序运行结果如下:
调用构造函数!
调用构造函数!
调用构造函数!
调用构造函数!
调用析构函数!
调用析构函数!
调用析构函数!
调用析构函数!
4. 命名空间
命名空间可作为附加信息来区分不同库中相同名称的函数、类、变量等。
4.1 定义命名空间
命名空间的定义使用关键字 namespace,后跟命名空间的名称
namespace namespace_name {
// 代码声明
}
为了调用带有命名空间的函数或变量,需要在前面加上命名空间的名称
name::code; // code 可以是变量或函数
通过代码看看命名空间如何为变量或函数等实体定义范围
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
int main ()
{
// 调用第一个命名空间中的函数
first_space::func();
// 调用第二个命名空间中的函数
second_space::func();
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Inside first_space
Inside second_space
4.2 using 指令
使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。
#include <iostream>
using namespace std;
// 第一个命名空间
namespace first_space{
void func(){
cout << "Inside first_space" << endl;
}
}
// 第二个命名空间
namespace second_space{
void func(){
cout << "Inside second_space" << endl;
}
}
using namespace first_space;
int main ()
{
// 调用第一个命名空间中的函数
func();
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Inside first_space
5. 模板
模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。
模板是创建泛型类或函数的蓝图或公式。库容器,比如迭代器和算法,都是泛型编程的例子,都使用了模板的概念。
5.1 函数模板
模板函数定义的一般形式
template <class type> ret-type func-name(parameter list)
{
// 函数的主体
}
下面是函数模板的实例,返回两个数中的最大值
#include <iostream>
#include <string>
using namespace std;
template <typename T>
inline T const& Max (T const& a, T const& b)
{
return a < b ? b:a;
}
int main ()
{
int i = 39;
int j = 20;
cout << "Max(i, j): " << Max(i, j) << endl;
double f1 = 13.5;
double f2 = 20.7;
cout << "Max(f1, f2): " << Max(f1, f2) << endl;
string s1 = "Hello";
string s2 = "World";
cout << "Max(s1, s2): " << Max(s1, s2) << endl;
return 0;
}
当上面的代码被编译和执行时,它会产生下列结果:
Max(i, j): 39
Max(f1, f2): 20.7
Max(s1, s2): World
5.2 类模板
我们也可以定义类模板。泛型类声明的一般形式如下
template <class type> class class-name {
.
.
.
}
下面的实例定义了类 Stack<>,并实现了泛型方法来对元素进行入栈出栈操作
#include <iostream>
#include <vector>
#include <cstdlib>
#include <string>
#include <stdexcept>
using namespace std;
template <class T>
class Stack {
private:
vector<T> elems; // 元素
public:
void push(T const&); // 入栈
void pop(); // 出栈
T top() const; // 返回栈顶元素
bool empty() const{ // 如果为空则返回真。
return elems.empty();
}
};
template <class T>
void Stack<T>::push (T const& elem)
{
// 追加传入元素的副本
elems.push_back(elem);
}
template <class T>
void Stack<T>::pop ()
{
if (elems.empty()) {
throw out_of_range("Stack<>::pop(): empty stack");
}
// 删除最后一个元素
elems.pop_back();
}
template <class T>
T Stack<T>::top () const
{
if (elems.empty()) {
throw out_of_range("Stack<>::top(): empty stack");
}
// 返回最后一个元素的副本
return elems.back();
}
int main()
{
try {
Stack<int> intStack; // int 类型的栈
Stack<string> stringStack; // string 类型的栈
// 操作 int 类型的栈
intStack.push(7);
cout << intStack.top() <<endl;
// 操作 string 类型的栈
stringStack.push("hello");
cout << stringStack.top() << std::endl;
stringStack.pop();
stringStack.pop();
}
catch (exception const& ex) {
cerr << "Exception: " << ex.what() <<endl;
return -1;
}
}
当上面的代码被编译和执行时,它会产生下列结果:
7
hello
Exception: Stack<>::pop(): empty stack
6. 预处理器
预处理器是一些指令,指示编译器在实际编译之前所需完成的预处理。
所有的预处理器指令都是以井号(#)开头
6.1 #define 预处理
#define 预处理指令用于创建符号常量。该符号常量通常称为宏
#define macro-name replacement-text
当这一行代码出现在一个文件中时,在该文件中后续出现的所有宏都将会在程序编译之前被替换为 replacement-text
#include <iostream>
using namespace std;
#define PI 3.14159
int main ()
{
cout << "Value of PI :" << PI << endl;
return 0;
}
看看预处理的结果
$ gcc -E test.cpp > test.p
...
int main ()
{
cout << "Value of PI :" << 3.14159 << endl;
return 0;
}
6.2 参数宏
可以使用 #define 来定义一个带有参数的宏
#include <iostream>
using namespace std;
#define MIN(a,b) (a<b ? a : b)
int main ()
{
int i, j;
i = 100;
j = 30;
cout <<"较小的值为:" << MIN(i, j) << endl;
return 0;
}
上面的代码被编译和执行时,它会产生下列结果
较小的值为:30
6.3 条件编译
有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。
#ifdef NULL
#define NULL 0
#endif
可以只在调试时进行编译,调试开关可以使用一个宏来实现
#ifdef DEBUG
cerr <<"Variable x = " << x << endl;
#endif
可以使用 #if 0 语句注释掉程序的一部分
#if 0
不进行编译的代码
#endif
6.4 C++ 中的预定义宏
代码示例
#include <iostream>
using namespace std;
int main ()
{
cout << "Value of __LINE__ : " << __LINE__ << endl; //会在程序编译时包含当前行号
cout << "Value of __FILE__ : " << __FILE__ << endl; //在程序编译时包含当前文件名
cout << "Value of __DATE__ : " << __DATE__ << endl; //会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
cout << "Value of __TIME__ : " << __TIME__ << endl; //会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。
return 0;
}
上面的代码被编译和执行时,它会产生下列结果
Value of __LINE__ : 6
Value of __FILE__ : test.cpp
Value of __DATE__ : Feb 28 2011
Value of __TIME__ : 18:52:48
7. 多线程
多线程是多任务处理的一种特殊形式,多任务处理允许让电脑同时运行两个或两个以上的程序
7.1 创建线程
我们用下面的代码来创建一个 POSIX 线程
#include <pthread.h>
pthread_create (thread, attr, start_routine, arg)
/*
thread: 指向线程标识符指针。
attr: 一个不透明的属性对象,可以被用来设置线程属性。您可以指定线程属性对象,也可以使用默认值 NULL。
start_routine: 线程运行函数起始地址,一旦线程被创建就会执行。
arg: 运行函数的参数。它必须通过把引用作为指针强制转换为 void 类型进行传递。如果没有传递参数,则使用 NULL。
*/
创建线程成功时,函数返回 0,若返回值不为 0 则说明创建线程失败
7.2 终止线程
我们用下面的代码来终止一个 POSIX 线程
#include <pthread.h>
pthread_exit (status)
pthread_exit 用于显式地退出一个线程。pthread_exit() 函数是在线程完成工作后无需继续存在时被调用
7.3 代码示例
#include <iostream>
// 必须的头文件
#include <pthread.h>
using namespace std;
#define NUM_THREADS 5
// 线程的运行函数
void* say_hello(void* args)
{
cout << "Hello Runoob!" << endl;
return 0;
}
int main()
{
// 定义线程的 id 变量,多个变量使用数组
pthread_t tids[NUM_THREADS];
for(int i = 0; i < NUM_THREADS; ++i)
{
//参数依次是:创建的线程id,线程参数,调用的函数,传入的函数参数
int ret = pthread_create(&tids[i], NULL, say_hello, NULL);
if (ret != 0)
{
cout << "pthread_create error: error_code=" << ret << endl;
}
}
//等各个线程退出后,进程才结束,否则进程强制结束了,线程可能还没反应过来;
pthread_exit(NULL);
}
// $ g++ test.cpp -lpthread -o test.o
执行程序,将产生下列结果
$ ./test.o
Hello Runoob!
Hello Runoob!
Hello Runoob!
Hello Runoob!
Hello Runoob!
7.4 向线程传递参数
通过结构传递多个参数。也可以在线程回调中传递任意的数据类型,因为它指向 void
#include <iostream>
#include <cstdlib>
#include <pthread.h>
using namespace std;
#define NUM_THREADS 5
struct thread_data{
int thread_id;
char *message;
};
void *PrintHello(void *threadarg)
{
struct thread_data *my_data;
my_data = (struct thread_data *) threadarg;
cout << "Thread ID : " << my_data->thread_id ;
cout << " Message : " << my_data->message << endl;
pthread_exit(NULL);
}
int main ()
{
pthread_t threads[NUM_THREADS];
struct thread_data td[NUM_THREADS];
int rc;
int i;
for( i=0; i < NUM_THREADS; i++ ){
cout <<"main() : creating thread, " << i << endl;
td[i].thread_id = i;
td[i].message = (char*)"This is message";
rc = pthread_create(&threads[i], NULL,
PrintHello, (void *)&td[i]);
if (rc){
cout << "Error:unable to create thread," << rc << endl;
exit(-1);
}
}
pthread_exit(NULL);
}
执行程序,将产生下列结果
$ g++ -Wno-write-strings test.cpp -lpthread -o test.o
$ ./test.o
main() : creating thread, 0
main() : creating thread, 1
Thread ID : 0 Message : This is message
main() : creating thread, Thread ID : 21
Message : This is message
main() : creating thread, 3
Thread ID : 2 Message : This is message
main() : creating thread, 4
Thread ID : 3 Message : This is message
Thread ID : 4 Message : This is message
写在后面
C++知识梳理告一段落,下面推荐一些相关的书籍
《C++ Primer》、《Effective C++》、《Thinking in C++》、《The Design and Evolution of C++》
还是那句话,语言只是一门工具,希望大家都能够在正确的场合使用最合适的工具
以下为我的微信公众号:技术经理的成长
会不定期进行更新,欢迎关注