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++》
还是那句话,语言只是一门工具,希望大家都能够在正确的场合使用最合适的工具

以下为我的微信公众号:技术经理的成长

会不定期进行更新,欢迎关注
C++学习笔记(3) - 高级编程