c++学习日志(7.8-7.15)

来源:互联网 发布:php 过滤非utf8字符串 编辑:程序博客网 时间:2024/06/02 23:48

/*学习内容来自《c++程序设计语言》(特别版) 裘宗燕*/

一.源文件和程序

1.分别编译

    程序是由好多源文件组成的,一般来说,一个程序就是一个文件的说法是行不通的。这样

做的好处是可以让我们更好的理解程序,假如一个程序就是一个文件,那么当这个文件和他所

依赖的某东西做了修改,那么整个程序都要重新编译。

    源文件的编译是这样的,首先进行的是预处理,包括宏的替换和文件#include包含,预处

理结束后的东西就是编译单位。

    分别编译能工作的前提是,在各个源文件里必须提供各种声明,以提供一些有关程序其他部

分类型的信息,在各个源文件和头文件里,这些声明要保持一致。连接器就是干这个工作的,

他用来将各个编译单位约束在一起。

2.连接

    所有的名字空间,类,函数都应该在他们出现的各个编译单位有适当的声明,而且他们都

应该引用同一个实体。声明包括定义式声明和非定义的声明:对于一个变量x:

           int x;                      //这是一个定义

           extern int x;            //声明,非定义

           extern int x = 5;     //是定义,带有初始式的声明就是定义,等价于 int x =

5;

    在一个程序里,一个变量只能定义一次,却可以有多个非定义的声明,但要保持类型的一

致:

           //f1.c

           int x = 1;

           int y = 1;

           //f2.c

           int x;                      //错拉,重复定义

           extern double y;     //错拉,类型不一致

     一个函数在使用之前必须声明,这与c语言不同,c语言可以调用未声明的函数。
     虽然一个变量只能定义1次,但在程序里,有很多不可避免的因素,使一些类型不得不出

现1次以上的定义:书上这部分不是太懂。
     存在“单一定义规则”(ODR);
     他的意思是,一个类,摸板,抑或一个inline函数,的俩个定义能够被接受为唯一一个定

义的实例:
     1.他们出现在不同的编译单位里;
     2.他们的所有词法相同;
     3.他们的词所代表的意义也相同;
ODR的存在是因为:
     //flie s.h
          struct S { int a;  char b; }
          void f(S*);
     //file1.c
          #include "s.h"
          //使用f()
     //file2.c
          #include "s.h"
          void f(S* p) { /* ... */ }  //f的实现
这样就让S的定义既出现在file1.c里和file2.c里成为理所当然的事了。
对上面的第3点出错的可能最大,所以要把头文件做的够强,把该有的名字都声明,抑或定义一

下。

     假如一个名字可以在与其定义所在不同的编译单位里使用,就是它具有外部连接,否则就

是具有内部连接:
     inline函数必须在需要用它的每个编译单位用完全一样的代码定义一遍。也就是说,

inline函数像名字空间一样,不存在不是定义的声明。
     const,typedef在默认的情况下具有内部连接:
            //f1.c
            const int x = 7;
            //f2.c
            const int x = 8;        //完全可以,内部连接就是互不干扰
     这样的东西会造成一些混乱,所以把他们放进头文件吧。
     当然,可以去掉这种默认,使const具有外部连接:
            //f1.c
            extern const int a = 77;
            //f2.c
            extern const int a;            // 这个a就是f1.c里的a
     无名名字空间效果很像内部连接。
     另外,static也表示使用内部连接。具体用法不清楚。

3.头文件
     #include "haha.h"的意思就是,用文件haha.h的内容取代#include所在的这一行。
     头文件里可以包括的东西:

命名名字空间          namespace N {/* ...*/}

类型定义                  struct Point { int x,y; };

模板声明                  template <class T> class z;

模板定义                  template <class T> class v {/*...*/};

函数声明                  extern int strlen( const char* );

在线函数定义          inline char get( char* p ) { return *p++; }

数据声明                  extern int a;

常量定义                  const float pi = 3.141593;

枚举                          enum Light { red, yellow, green };

名字声明                  class Matrix;

包含指令                  #include <algorithm>

宏定义                      #define VERSION 12

条件编译指令          #ifdef  __cplusplus

注释                          /* check for end of file*/

 


     头文件里不该有的东西:

常规的函数定义      char get( char* p ) { return *p++; }

数据定义                  int a;

聚集量定义              short tbl[] = { 1,2,3 };

无名名字空间          namespace {/*...*/}

导出的模板定义      export template <class T> f( T t ) {/*...*/}
     标准库头文件,是由系统提供的编程工具。
4.非c++代码的连接
     extern "C" char* strcpy(char*, const char*);
     extern char* strcpy(char*, const char*);
     以上俩段的区别就是连接约定不同。"C"表示一种约定,却不是语言。
     连接约定不影响调用函数的语意。
     为一组声明描述连接约定的语法:
           extern "C"{
                /*声明*/
           }
     这种结构被叫做连接块,可以用来包裹起整个的c头文件,使之适合c++:
           extern "C"{
           #include <string.h>
           }
     还有一种条件编译的方式
     用连接块包裹的变量,其作用域和存储类不会受到影响
           extern "C" {
                 int g1;     //是定义
           }
           extern "C" int g3; //声明,却不是定义
     具有c连接的库,包含到选定的名字空间,不去污染全局名字空间是什么意思?
c++实体的连接必须将名字空间考虑在内,那么c的连接不用考虑名字空间?于是,c就能包含到

选定的名字空间?
函数指针情况:
typedef int (*FT) ( const void*, const void* );   //FT具有C++连接
void isort( void* p, size_t n, size_t sz, FT cmp ); //cmp具有C++连接

extern "C" {
 typedef int (*CFT) ( const void*, const void* );  //CFT具有C连接
 void qsort( void* p, size_t n, size_t sz, CFT cmp ); //qsort具有C连接,cmp 

                                                           //具有C连接
}


void xsort( void* p, size_t n, size_t sz, CFT cmp ); //cmp具有C连接,xsort是C++连接
extern "C" void ysort( void* p, size_t n, size_t sz, FT cmp ); //cmp具有C++连接,  

                                                            //ysort是C连接

int compare( const void*, const void* );   //compare具有C++连接
extern "C" int ccmp( const void*, const void* );  //ccmp具有C连接

void f( char* v, int sz )
{
 qsort( v, sz, 1, &compare );  //具有C++连接的函数作参数,传递到本应是C连接 

                                    //的形参,error
 qsort( v, sz, 1, &ccmp );     //具有C连接的函数作参数,传递到C连接的形参, 

                                    //ok

 isort( v, sz, 1, &compare );  //具有C++连接的函数作参数,传递到C++连接的形 

                                    //参,ok
 isort( v, sz, 1, &ccmp )      //具有C连接的函数作参数,传递到本应是C++连接 

                                    //的形参,error
}
在为一个声明刻画连接约定时,这个约定将应用于此声明引进的所有函数类型,函数名,变量

名。对这句的理解是:CFT具有c连接,由CFT引进的cmp也具有了C连接了。上面的error不一定

是error,只是要当心。
5.头文件的组织
   有俩种方式:单一头文件和多头文件:
   桌面计算器程序的俩种组织方式。
   单一头文件的完整代码:

//dc.h/************************************************************************                              单头文件*************************************************************************//*有关异常的名字空间*/namespace Error { struct zero_divide { };            //除0错误    struct syntax_error {              //语法错误  const char* p;  syntax_error( const char* q ) { p = q; } };}/*有关词法分析的名字空间*/#include namespace Lexer { enum Token_value {                 //词法符号:数字,名字,运算符等             NUMBER,        NAME,        END,        PLUS = '+',    MINUS = '-', MUL = '*',   DIV = '/',  LP = '(',      RP = ')',    PRINT = ';', ASSIGN = '=' }; extern double number_value;        //声明,存储数字的变量 extern std::string string_value;   //声明,存储名字的变量 extern Token_value curr_tok;       //声明,当前的词法符号 Token_value get_token();           //声明,取得curr_tok的函数}/*有关语法分析的名字空间*/namespace Parser { double prim( bool get );           //声明,处理初等项 double term( bool get );           //声明,处理乘除 double expr( bool get );           //声明,处理加减            }/*有关符号表的声明*/#include extern std::map table; //声明,一个〈字符串,值〉对/*有关驱动的一些声明所属的名字空间*/namespace Driver { void skip();                       //声明,当出错时跳跃到PRINT的函数 extern int no_of_errors;           //声明,最终程序返回给系统的值}//lexer.cpp/*********************************************************************                 词法分析的定义*********************************************************************/#include "dc.h"#include using std::cin;#include Lexer::Token_value Lexer::curr_tok;double Lexer::number_value;std::string Lexer::string_value;Lexer::Token_value Lexer::get_token()             //词法分析{ char ch; do{                                           //忽略除/n以外的空白单词  if( !cin.get(ch) ) return curr_tok = END; }while( ch != '/n' && isspace( ch ) ); switch( ch ) { case '/n':                                    //可以用;或'/n'结束一个表达式 case ';':  return curr_tok = PRINT; case '+':  case '-':  case '*':  case '/':    //处理常见的符号    case '=':  case '(':  case ')':    return curr_tok = Token_value( ch ); case '0':  case '1':  case '2':  case '3':    //处理number_value case '4':  case '5':  case '6':  case '7': case '8':  case '9':  cin.putback( ch );  cin >> number_value;  return curr_tok = NUMBER; default:                                      //处理string_value  if( isalpha(ch) ) {   string_value = ch;   while( cin.get( ch ) && isalnum( ch ) ) string_value += ch;   cin.putback(ch);                      //这一句很重要,使得                                                              //这个ch可被下一次                                                              //get_token用到   return curr_tok = NAME;  }  throw Error::syntax_error( "非法单词!" ); }}//parser.cpp/************************************************************************                   语法处理************************************************************************/#include "dc.h"using namespace Lexer;                   //为了偷懒的使用词法分析里的名字/*处理初等项,初等项里的词法层次最低,在用赋值和括号时却要调用高层次的expr*/double Parser::prim( bool get ){ if( get ) get_token(); switch( curr_tok ) { case NUMBER: {  double v = number_value;  get_token();  return v; } case NAME:  {  double& v = table[ string_value ];  if( get_token() == ASSIGN ) v = expr( true );  return v; } case LP:  {  double e = expr( true );  if( curr_tok != RP ) throw Error::syntax_error( "缺少右)号" );  get_token();  return e; } case MINUS:  return -prim( true ); default:  throw Error::syntax_error( "未知错误" ); }}/*处理乘和除*/double Parser::term( bool get ){ double left = prim( get ); for( ; ; ) {  switch( curr_tok ) {  case MUL:   left *= prim( true );   break;  case DIV:   if( double d = prim( true ) ) {    left /= d;    break;   }   throw Error::zero_divide();  default:   return left;  } }}/*处理加和减,程序里表现了对一行用';','/n'结尾的语法句的总处理*/double Parser::expr( bool get ){ double left = term( get ); for( ; ; ) {  switch( curr_tok ) {  case PLUS:   left += term( true );   break;  case MINUS:   left -= term( true );   break;  default:   return left;  } }}//table.cpp/*******************************************************************                  符号表*********************************************************************//*关于map的定义,在大程序里,这样的全局变量少用*/#include "dc.h"std::map table;//main.cpp/*********************************************************************************                   驱动函数*********************************************************************************/#include "dc.h"#include                           //cin,cerr,cout的使用using std::cin;using std::cerr;using std::cout;int main(){ table[ "pi" ] = 3.1415926535897932385;   //常用的符号值对可以添加 table[ "e" ] = 2.7182818284590452354;     using namespace Lexer;                   //直接单纯的使用名字空间Lexer里的东西 using namespace Error;                   //直接单纯的使用名字空间Error里的东西 while( cin ) {  try {   get_token();   if( curr_tok == END ) break;   if( curr_tok == PRINT ) continue;   cout << Parser::expr( false ) << '/n';   //一个连环调用,显示以';','/n'为结尾表达式的值  }  catch( zero_divide ) {            cerr << "0不可以作除数!/n";             //捕捉到除0的错误   if( curr_tok != PRINT ) Driver::skip();  }  catch( syntax_error e ) {                    //捕捉到语法错误   cerr << "语法错误:" << e.p << '/n';   if( curr_tok != PRINT ) Driver::skip();  } } return Driver::no_of_errors;}int Driver::no_of_errors; //定义/*错误处理,将这一行舍去*/void Driver::skip(){ no_of_errors++; while( cin ) {  char ch;  cin.get(ch);  switch( ch ) {  case '/n':  case ';':   return;  } }}


单头文件适合的是小程序的情况,当程序很大时,这样做很不好。
多头文件的情况是:
一个模块,可分为用户界面,实现界面,和实现。
例如对于parser,可分为:parser.h /*用户界面*/
parser_impl.h /*实现界面*/ 表示实现的共享的东西
parser.c  /*实现*/
多头文件提供了更好的局部性。

6.包含保护符
  #ifndef ABC_H
  #define ABC_H
   /*...*/
  #endif
  包含保护符使得重新编译的冗余减少了,因此被广泛的使用。

7.程序
  一组分别编译的单位由连接器组合起来,就是一个程序。程序的过程就是main()的执行过程。原则上在所有函数之外的变量,应该在main()调用之前完成初始化。在一个编译单位里的这种变量,按他们定义的顺序初始化。没有初始式的内部类型默认初始化为0。而在不同的编译单位里的全局变量,初始化顺序是没有保证的。全局变量的初始化抛出的异常无法捕捉。
全局变量有一种很好的替代物:通过函数返回的引用。
  int& use_count()
  {
        static int uc = 0;
        return uc;
  }
  void f()
  {
        cout << ++use_count();
        //...
  }
static变量只被初始化1次。
而有些这样的变量需要运行时初始化。那么以上说的都不是运行时的?
   程序的终止:
   1.exit()将调用静态对象的析构函数。abort()则不。
   2.调用exit()结束程序,调用它的函数及其调用者里的局部变量的析构函数都不会执行
1,2点看似矛盾。
   3.atexit()函数,程序在终止前可以执行一些代码。
提供给atexit()参数的函数不能有返回值也不能有参数,atexit()通过返回非0值表明达到最大限度?

/*红字表明疑问,不理解*/

 

 

 


 

原创粉丝点击