C++学习笔记-基本语法

变量

  • POD类型
    POD类型(Plain old data structure): 在源代码兼容于ANSI C时非常重要。POD对象与C语言的对应对象具有共同的一些特性,包括初始化、复制、内存布局、寻址。具体分为以下两类:
    • 标量类型(scalar type):
      • 算术类型(arithmetic type):
        • 整数类型(integral type):
          • 有符号整数类型 (signed char, short, int, long),
          • 无符号整数类型(unsigned char, unsigned short, unsigned int, unsigned long),
          • 字元类型char与宽字元类型wchar_t
          • 布林类型bool
        • 浮点类型(floating type)
      • 枚举类型(enumeration type)
      • 指针类型(pointer type):
        • 空指针类型(pointer-to-void, 即void *)
        • 对象指针(pointer-to-object), 以及指向静态数据成员的指针(pointer-to-static-member-data)
        • 函数指针(pointer-to-function), 以及指向静态成员函数的指针(pointer-to-static-member-function)
      • 成员指针类型(pointer-to-member type):
        • 非静态数据成员指针(pointer-to-nonstatic-member-data)
        • 非静态成员函数指针(pointer-to-nonstatic-member-functions)
    • POD类类型: 在C++11后, 把POD类类型的判断标准推广为两大类: 平凡以及标准布局。当且仅当一个类或结构体同时满足平凡和标准布局时,属于POD类类型。
      • 平凡类(trival): 可以静态初始化、可以用memcpy直接复制数据而不是必须用拷贝构造函数。其生存期始于它的对象的存储被定义,无须等到构造函数完成。
        判断标准: 当且仅当一个类或结构体的所有特殊成员函数(构造函数、拷贝构造函数、移动构造函数、赋值运算符、移动运算符、析构函数)均使用编译器提供的、或是显式初始化为default(等同于使用编译器提供的)时,该类型是一个平凡类/结构体。
        我们说一个成员函数是平凡的,当且仅当该成员函数不是用户自定义的,并且它属于的类满足以下条件:
        • 没有虚函数或虚基类
        • 没有基类包含非平凡的构造函数、赋值运算符或析构函数
        • 没有数据成员包含非平凡的构造函数、赋值运算符或析构函数
      • 标准布局的类(standard-layout): 意味着它是有序的并且其成员的内存布局兼容于C语言。
        判断标准: 一个标准布局的类需满足以下条件:
        • 没有虚函数或虚基类
        • 所有非静态数据成员有相同的访问控制(public, private, protected)
        • 所有非静态成员都是标准布局的
        • 所有基类都是标准布局的
        • 任何基类不能与该类第一个非静态数据成员类型相同
        • 满足下列条件中的任意一个:
          • 没有基类包含非静态数据成员
          • (继承体系中)最底层的类不包含非静态数据成员, 且不多于一个基类包含非静态数据成员
      • 示例:
          struct B{  
          protected:  
              virtual void Foo() {}  
          };  
          // Neither trivial nor standard-layout  
          struct A : B{   
              int a;  
              int b;  
              void Foo() override {} // Virtual function  
          };  
          // Trivial but not standard-layout  
          struct C{   
              int a;  
          private:  
              int b;  // Different access control  
          };  
          // Standard-layout but not trivial  
          struct D{   
              int a;  
              int b;  
              D() {} //User-defined constructor  
          };  
          struct POD{  
              int a;  
              int b;  
          };  
          int main(){  
              cout << boolalpha;  
              cout << "A is trivial is " << is_trivial<A>() << endl; // false  
              cout << "A is standard-layout is " << is_standard_layout<A>() << endl;  // false  
              cout << "C is trivial is " << is_trivial<C>() << endl; // true  
              cout << "C is standard-layout is " << is_standard_layout<C>() << endl;  // false  
              cout << "D is trivial is " << is_trivial<D>() << endl;  // false  
              cout << "D is standard-layout is " << is_standard_layout<D>() << endl; // true  
              cout << "POD is trivial is " << is_trivial<POD>() << endl; // true  
              cout << "POD is standard-layout is " << is_standard_layout<POD>() << endl; // true  
              return 0;  
          }
        
  • 定义和声明的区分
    类内:
      int a; 声明
      void func(); 声明
      void func2(){} 定义
    
    类外:
      int a; 定义
      int a = 0; 定义+初始化
      extern int a; 声明
    
  • 名称空间: 相当于预先把一些变量或类的名字声明出来, 以便禁止用户再使用这些相同的名称来命名自己的变量或类而造成混乱. 如 cin 和cout这两个关键字都是在std这个名称空间中定义的(输入和输出流)

函数

  • 在函数的参数表中, 如果排在前面的参数被赋予默认值, 则排在其后的所有参数都必须有默认值
  • 相似地, 调用函数时省略了某一个参数, 那么这个参数之后的所有参数都必须被省略, 这个参数之前的参数不能被省略( 否则会出现二义性)
  • 默认实参可用全局变量来赋值, 但不可用局部变量
  • 与其他函数不一样, 内联函数和constexpr函数可以被多次定义, 但多次定义必须一致, 所以这两类函数通常被 放在头文件中
  • 由于定义一个指针数组的形式为
    int (*pArr)[10];
    因此定义返回一个数组的函数的形式为
    int ( *GetArr(param_list) ) [10]{ }
    在C++11中, 允许返回值后置的形式:
    auto GetArr(param_list) -> int (*) [10] { }
    或者使用decltype运算符( 假设之前已经存在定义语句 int myArr[10]; ) :
    decltype(myArr) * GetArr(param_list) { }
  • 返回一个数组的引用:
      string ( &f(void) )[10] {
          static string arr[10]; 
          return arr; 
      }
    
    返回的必须是静态变量(的引用)或定义在该函数外的全局变量(的引用), 不能是局部变量
    否则一旦出了函数的作用域局部变量所占用的内存就被回收了, 返回的引用指向的内存是不可用的
  • const放在函数后面修饰函数会影响函数签名, 但const修饰返回值则不会影响

输入输出流

  • cout是一个(智能)对象,表示输出流,对象属性包括一个插入运算符<<,可以将其右侧的信息插入到输出流中.而且其插入的信息可以是任意类型的(不用像printf那样还需要提前知道类型)
  • 类似的,cin表示输入流这个对象,使用>>从输入流中抽取字符以便>>右边的变量接收,并且能将键盘输入的字符自动转换为变量能够存储的形式
  • 类之于对象就像类型之于变量,即 类定义描述某种数据的构成等全部属性及其用法(即可执行的操作),而对象是根据数据格式创建的实体.这样来看,cout就是ostream类定义(iostream文件的另一个成员)的一个对象.ostream描述了该类的对象及可对它进行的操作,如将数据或字符串插入到输入流中
  • 读入字符串的方法:
方法 特征
cin >> s 读到不可见字符如’ ‘, ‘\n’, ‘\t’等停止
getline (istream& is, string& str, char delim) 可用于读取不定长的字符, 直接写入到字符串变量中
istream.getline (char* s, streamsize n, char delim ) 可用于读取固定个数的字符, istream.get()也有相同参数表的重载
  • 一旦一个流发生错误, 其上后序的IO操作都会失效, 因此应该在使用一个流之前检查它是否处于正常状态:
    如 while( cin >> input ){} 如果输入成功, 流保持有效状态, 返回值为真
  • 将标准输入重定向为一个字符串 :
      stringstream ss("Content");
      cin.rdbuf( ss.rdbuf() );
    
    注意这里 ss定义的时候不能用构造函数 stringstream(const & string, size_t), 为什么?
  • 只用 ios::out模式打开时, 会把原来的文件清空, 因此要想只重写文件的一部分必须用ios::in | ios::out 模式来打开
  • std::ios::sync_with_stdio(false)的作用:
    标准输入, 输出流会先把输入, 输出的东西存入缓存区再输出, 因此效率比printf, scanf要低, 增加了这一句之后就加快了输入输出的效率, 有时候可能是解决TLE的关键.
    相关解释:

    Synchronizes the C++ streams with the standard I/O system. The first time this function is called, it resets the predefined streams (cin, cout, cerr, clog) to use astdiobuf object rather than a filebuf object. After that, you can mix I/O using these streams with I/O using stdin, stdout, and stderr. Expect some performance decrease because there is buffering both in the stream class and in the standard I/O file system.
    After the call to sync_with_stdio, the ios::stdio bit is set for all affected predefined stream objects, and cout is set to unit buffered mode.

    实测数据:

编译链接

  • 关于多个源文件(.cpp)公有变量的使用方法
    若var要作为公有变量, 则应该且必须仅在一个.cpp文件内定义
      extern int var = 0;
    
    并且在所有要用到该变量的源文件内对该变量进行声明extern int var; (也有不需要该声明的办法, 因为gcc在编译的时候是按照makefile中文件出现的顺序来寻找符号的, 因此只要把定义该变量的源文件放在其他源文件的前面即可)
    注:
    • 实际上如果不加任何修饰, 编译器对全局变量的声明方式默认为extern, 因此上述extern均可省去(但为了程序的易读性, 应显式标明)
    • const型的变量不适用上述方法, 需要特殊规则. 因为const型变量默认修饰符为static, 因此若需要把const 变量作为一个公有变量, 有两种方法:
      1. 在一个源文件内定义extern const int PI = 3.141592653589793, 在其他需要用到PI的源文件内声明extern const int PI; (类型必须对应一致, 否则当做这里的PI未定义)
      2. 在一个头文件内定义extern const int PI = 3.141592653589793, 在其他源文件内包含这个头文件即可(这样相当于在每个源文件中分别定义一个const型的变量, 因此可以发现每个源文件中该const变量的地址是不同的, 但其值是永远一致的)
  • 不要将函数的定义和变量的声明放到头文件中, 头文件应包含的内容为:
    函数原型, 使用#define或const定义的符号常量, 结构声明, 类声明, 模板声明, 内联函数
  • 注意在全局变量不用const限定时, 链接性为外部的(其他文件可访问), 但是一旦用const限定之后, 其链接性就变成了内部的, 相当于加了static

内存模型

  • C/C++内存段分配
    • .rodata段:存放只读数据,比如printf语句中的格式字符串和开关语句的跳转表。也就是你所说的常量区。
      例如,全局作用域中的 const int ival = 10,ival存放在.rodata段
      再如,函数局部作用域中的printf(“Hello world %d\n”, c);语句中的格式字符串”Hello world %d\n”,也存放在.rodata段
    • .text段:存放已编译程序的机器代码。
      注意:程序加载运行时,.rodata段和.text段通常合并到一个Segment(Text Segment)中,操作系统将这个Segment的页面只读保护起来,防止意外的改写。
    • .data段:存放已初始化的全局变量。而局部变量在运行时保存在栈中,既不出现在.data段,也不出现在.bss段中。就是你所说的全局区。
      例如:全局作用域中的int ival = 10,static int a = 30,以及局部作用域中的static int b = 30,这3个变量均存放在.data段中。注意,局部作用域中的static变量的生命周期和其余栈变量的生命周期是不同的。
    • .bss段:存放未初始化的全局变量。在目标文件中这个段不占据实际的空间,它仅仅是一个占位符,在加载时这个段用0填充。目标文件区分初始化和未初始化变量是为了空间效率:在目标文件中,未初始化变量不需要占据任何实际的磁盘空间。全局变量如果不初始化则初值为0,同理可以推断,static变量(不管是函数里的还是函数外的)如果不初始化则初值也是0,也分配在.bss段。
      例如,全局作用域中的int ival;
      ival显然存放在.bss段
      注意:.data和.bss在加载时合并到一个Segment(Data Segment)中,这个Segment是可读可写的。
    • 栈:函数的参数和局部变量是分配在栈上(但不包括static声明的变量)。在函数被调用时,栈用来传递参数和返回值。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。
    • 堆:用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc/free等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张)/释放的内存从堆中被剔除(堆被缩减)
      注:
      1. 类的成员函数存放在代码段。
      2. 在C++中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。注意要考虑对齐。
  • 线程存储持续性(C++11) : 如果变量使用thread_local 声明, 则其生命周期和其所属线程的生命周期一样长
  • 静态存储持续性 : 用static关键字声明, 静态变量, 编译器将分配固定的内存块(堆)来存储所有的静态变量
    动态存储持续性 : 用new关键字分配的变量将一直存在(于堆中), 直到用delete释放
  • 存储说明符: auto(在C++11中不再是说明符, 被重定义为自动推断变量的类型), register, static, extern, thread_local, mutable
  • cv限定符:
    • const
    • volatile(表明程序代码没有对内存单元进行修改, 其值也可能发生变化)
      例如, 将一个指针指向某个硬件位置, 其中包含了来自串行端口的时间或信息, 这样指针指向的那段内存就可能会被硬件修改(而不是程序本身). 如果不对该指针加volatile声明, 那么编译器可能在两次访问该变量时进行优化, 即不对该变量查找两次, 而是直接取前一次的值; 若加了volatile声明则编译器将不会进行这种优化
  • C语言三个分配内存的函数(都是在堆区申请内存)的区别:
函数 特征
malloc(int total_size); 常用, 分配完的内存需要自己初始化
calloc(int array_size, int element_size); 特点是分配完的空间都是经过初始化的,即全为0, 常用于分配数组
realloc(void * ptr, int total_size); 要求ptr必须为指向堆的指针, 即ptr必须为malloc或calloc申请的指针(若ptr指向内存区域的size小于参数total_size则重新分配一块新的内存, 并把原来ptr指向的内容拷贝到新的区域,否则不操作)。
  • 使用T * p = new(addr) T(); 用一个指针将类T实例化, 并在new运算符后添加一个地址参数, 则new会在所给的地址参数指向的内存区域为T分配内存. (当然p必须是足够大的而且未被使用的)
  • 申请内存时,指针所指向区块的大小这一信息,其实就记录在该指针的周围
  • 如果一个指针申请内存时new/malloc了S个字节, 如ptr = new char[S];
    然而对第6个字节的位置进行了写操作, 如 ptr[5] = 0;
    则被视为未定义行为, 可能的表现有:
    • 触发SIGTRAP中断(Dev cpp, MinGW 32bit)
    • 调用delete[] ptr后无返回, 即无限循环(MSVC)
    • VS的输出日志显示CRT detected that the application wrote to memory after end of heap buffer.
  • delete关键字
    • delete ptr代表用来释放内存, 且只用来释放ptr指向的内存。
    • delete[] rg用来释放rg指向的内存,还逐一调用数组中每个对象的destructor。
      对于像int/char/long/int/struct等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的。
      但是如果是C++对象数组就不同, 用delete 删除 new[] 申请的对象, 虽然*也会释放整个数组的内存
      , 但是只会调用第一个对象的析构函数