C++基础

CPP Primer Plus v6

内存模型和命名空间

单独编译

  • 头文件
  • Gurading programing
  • 单独编译与链接

存储周期

Cpp采用四种存储方案:

  • 自动存储持续性 函数临时变量
  • 静态存储持续性 static (静态存储区)
  • 动态存储持续性 new delete (堆)
  • 线程存储持续性 thread_local声明

单定义规则(CPP)

One Definition Rule: 变量只有一次定义机会.

CPP提供了两种变量声明:

  • 定义声明(definition declaration) 给变量分配地址
  • 引用声明(reference declaration) 引用已有变量
int a = 10; //&a = 0x7ffef99019dc definition declaration
int &b = a; //&b = 0x7ffef99019dc reference declaration

外部变量的声明

对于定义声明的extern可以缺省, 但是对于声明外部变量的文件extern必须要声明.

存储说明符

  • auto(deprecated) C++11中不再是说明符 而是自动变量类型的自动推导.
  • register C++11中只是显示声明这个变量是自动变量
  • static
  • extern
  • thread_local
  • mutable 即使结构体为const变量但是某个成员可以被修改.

cv-qualifier

  • const
  • volatile

注意: 在cpp中const与C有略微不同, 默认情况下全局变量的链接性是外部的, 而被const修饰
的全局变量的链接性是内部.

const int fingers = 10; // 与 static const int fingers = 10; 等效

那么解决的做法就是将此语句放到头文件中.
或者用extern const 代替.

函数

函数的存储持续性都是静态的, 并且链接性默认为外部.(可以类比全局变量)
可以通过static声明链接性为内部的.静态函数会覆盖外部定义.

语言的链接性

C中不会出现同名函数, 而cpp中多态允许多个函数同名, 那么需要在编译期间对函数符号重名
比如spiff(int)会被变成_spoff_i, 这种称为cpp语言的链接性.

由于C和cpp的语言链接性的差异, 那么在cpp寻找C库的函数就要明确规定使用C的方式

extern "C" void spiff(int); // C protocol for name look-up
extern void spoff(int);     // cpp protocol for name look-up
extern "C++" void spaff(int);// cpp protocol for name look-up

动态存储

  1. new 并初始化

    int *p = new int(6); // only for embeded type(int double long short...)
    // or int *p = new int{6};
    struct where {double x; double y; double z;};
    where *one = new where{2.5,2.5,2.5};
    int *ar = new int[4] {1,2,3,4};
    
  2. new 失败

    早期返回NULL, 如今C++11引发异常std::bad_alloc

  3. 定位new运算符(设置初始位置)
    可以通过new (replacement)class来指定位置.
    如下:

    #include <new>
    struct chaff{
        char dross[20];
        int slag;
    };
    char buffer1[50];
    char buffer2[500];
    int main(){
       char *p1, *p2;
       int *p3, *p4;
       p1 = new chaff;             // heap allocated
       p2 = new int[20];           // heap allocated
       p3 = new (buffer1) chaff;   // static area
       p4 = new (buffer2) int[20]; // static areas
    }
    
  4. 命名空间

    • 使用已命名的名称空间中声明的变量,而非外部全局变量, 静态全局变量.
    • 开发的函数库或者类库将其放于一个命名空间中.(标准库函数放置于std中)
    • 尽量不在头文件中使用using
    • 导入名称时, 选择作用域解析符或using
    • using首选将其作用域设置为局部而非全局

异常机制

C++中的异常机制, 可以使用来自<cstdlib>中的abort()强制向std::cerr发送"abnormal
program termination", 退出当前程序. 或者采取返回异常码, 或者使用try-catch结构.

出现异常时使用throw语句简介跳转到try块结束处, 匹配catch语句中的异常类型.可以是字符串
或者用类表示的异常.

异常规范(已弃用)

throw出现在函数声明或定义中, 指导这个函数需要使用try-catch结构, 并且标注可能发生的
异常类型.如:

double harm(double a) throw(bad_thing); // may throw bad_thing exception
double marm(double) throw();  // dosen't throw a exception

但是这类声明很难检查, 如marm()可能本身不会发生异常, 但是可能会调用发生异常的函数.

C++11 提供了noexcept指出函数不会发生异常.

double marm(double) noexcept;  // dosen't throw a exception

栈退解(unwinding stack)

当try块没有直接调用引发异常的函数, 而是通过间接调用引发异常的函数,则程序从引发异常的函数
直接跳回包含try块的函数中, 这涉及到了栈退解.

栈退解: 当前函数出现异常而终止, 程序将释放栈中的内存, 但不会在释放栈的第一个返回地址就停止
, 它会一直释放直到找到第一个位于try块的返回地址, 随后将控制权转到块尾的异常处理程序(catch块)

PS: 传递异常类会传递一个临时拷贝, 即使异常规范或者catch块中是引用类型, 原理在于触发异常的函数会被销毁.
这里使用看似多此一举的引用的目的是为了基类引用可以执行派生类对象, 假设有一组通过继承关联的异常模型, 则通过
基类引用可以匹配任何派生类对象(注意catch顺序).

exception类

<exception>文件定义了一些标准异常类, 其中exception为基类, 你可以自定义其派生类为自定义异常类
,也可以在函数中直接引发exception(其虚拟成员函数what()返回错误字符串, 可重载)

  1. <stdexcept>
    • logic_error 典型的逻辑错误, 可修改编程逻辑避免
      • domain_error
      • invalid_argument
      • length_error
      • out_of_bounds
    • runtime_error 运行期间难以预计和防范的错误, 无法避免
      • range_error
      • overflow_error
      • underflow_error
  2. new -- bad_alloc
    对于使用new导致的内存分配问题, 引发bad_alloc.
    对于老式编译器返回null指针的做法可以用new(std::nothrow)代替.

未捕获异常(Unexpected exception)

对于未捕获异常, 程序会调用terminate(), 继而默认调用abort()退出.
可以通过set_terminate()修改默认的调用(不使用abort).
注意: terminate_handler是一个无参数无返回值的函数指针.
例如:

#include<exception>
using namespace std;
void myQuit(){
    cout << "Terminating due to uncaught exception\n";
    exit(5);
}
set_terminate(myQuit);

对于在异常规范中不存在的异常, 会触发unexpected(), 这个函数会调用terminate(),
你可以通过set_unexpected()修改默认的调用.

RTTI(RunTime Type Identification)

确定指针指向的对象.

三个支持RTTI的元素: dynamic_cast运算符, type_id运算符 和type_info结构.

RTTI只适用于包含虚函数的类, 只有对于此情况才应该将派生对象的地址赋给基类指针.

dynamic_cast

回答了"是否可以安全地将对象的地址赋给特定类型的指针"的问题.
安全的要求: 指针类型和对象类型(直接或间接基类类型)相同的类型转换才是安全的

#include <iostream>
#include <memory>

class Grand {
 public:
  virtual void foo() { std::cout << "foo-Grand" << std::endl; }
};
class Superb : public Grand {
 public:
  virtual void foo() { std::cout << "foo-Superb" << std::endl; }
};
class Magnificent : public Superb {
 public:
  void foo() { std::cout << "foo-Magnificent" << std::endl; }
};
int main(int argc, char const *argv[]) {
  using namespace std;
  shared_ptr<Grand> pg = make_shared<Grand>();
  shared_ptr<Grand> ps = make_shared<Superb>();
  shared_ptr<Grand> pm = make_shared<Magnificent>();
  shared_ptr<Grand> p1 = (shared_ptr<Grand>)pm;   //安全
  Magnificent *p2 = (Magnificent *)pg.get();      //不安全
  Superb *p3 = (Magnificent *)pm.get();           // 安全
  Superb *p4 = dynamic_cast<Superb *>(pg.get());  // 不安全 返回nullptr
  shared_ptr<Superb> p5 = dynamic_pointer_cast<Superb>(pg);
  p1->foo();
  p2->foo();
  p3->foo();
  if (p4)
    p4->foo();
  else
    std::cout << "\"Superb *p4 = dynamic_cast<Superb *>(pg.get());\" not safe"
              << std::endl;
  if (p5)
    p5->foo();
  else
    std::cout << "\"shared_ptr<Superb> p5 = "
                 "dynamic_pointer_cast<Superb>(pg);\" not safe"
              << std::endl;
  return 0;
}

output:

|| foo-Magnificent
|| foo-Grand
|| foo-Magnificent
|| "Superb *p4 = dynamic_cast<Superb *>(pg.get());" not safe
|| "shared_ptr<Superb> p5 = dynamic_pointer_cast<Superb>(pg);" not safe

注意对引用类型也可使用dynamic_cast, 只不过失败不是返回0而是抛出bad_cast异常

#include<typeinfo> // for bad_cast
try {
    Superb &rs = dynamic_cast<Superb &>(rg);
}
catch(bad_cast &a){
   pass;
}

typeidtype_info

typeid返回一个type_info对象的引用.重载了==!=用于判断, type_info有成员
name()来表示类的名称.

类型转换运算符

C语言中的类型转换过于松散, 所以C++中涉及类型转换, 强烈建议使用以下四种.

dynamic_cast (派生类转换为可访问基类, 从下至上)

dynamic_cast<type_name>(expression)

High *ph = new High();
Low *pl = dynamic_cast<Low *> ph; // 仅当Low是High的可访问基类

static_cast

static_cast<type_name>(expression)

仅当type_name可以被隐式转换为expression所属类型或逆之才合法.

  • 编译器隐式执行的任何类型转换都可以由static_cast来完成,比如int与float、double
    与char、enum与int之间的转换等。
  • 由派生类到基类的转换是合法的, 可显式进行.
  • 从基类到派生类的转换是合法的, 虽然可能不安全, static_cast不做类型检查.

const_cast (改变为const, volatile 属性)

const_cast<type_name>(expression)

High bar;
const High *pbar = &bar;
High *pb = const_cast<High *> (pbar); //valid 删除了const标签
const Low *pl = const_cast<const Low *>(pbar); // invalid type_name和expression必须相同

const_cast可以改变指针的const属性, 但是对于指向const对象的指针无法改变.如下:

int pop1 = 1;
const int pop2 = 2;
const int *cr_pop1 = &pop1;
const int *cr_pop2 = &pop2;
int *pc = const_cast<int *>(cr_pop1);
++*pc;
pc = const_cast<int *>(cr_pop2);
++*pc;
cout << pop1 <<" " << pop2;
// pop1 = 2 pop2 = 2 pop2不会变化

reinterpret_cast

reinterpret_cast<type_name>(expression)

强制翻译,如下:

struct dat{
short a,b;
};
long value = 0xA224B118;
dat *pd = reinterpret_cast<dat *>(&value);
cout << hex << pd->a;

string类

构造

  • NBTS(Null-terminated string)
  • string::npos (max unsigned int)
  • size_type
Constructor Desctription Example output
string(const char *s) string one("hello") hello
string(size_type n, char c) string two(3,'a') aaa
string(const string &str) string three(one) hello
string() string four()
string(const char *s, size_type n) string five("hello", 4) hell
string(const char *s, size_type pos = 0, size_type n=npos) string six("hello",1,2) el
string(Iter begin, Iter end) string seven(one.begin(), one.end()) hello
string(string &&str) noexcept string eight(one+two) helloaaa
string(initializer_listil) string nine({'a','b','c'}) abc

Example:

#include <iostream>
#include <memory>
#include <string>
int main(int argc, char const *argv[]) {
  using namespace std;
  shared_ptr<string> p[9];
  p[0] = make_shared<string>("hello");
  p[1] = make_shared<string>(3, 'a');
  p[2] = make_shared<string>(*p[0]);
  p[3] = make_shared<string>();
  p[4] = make_shared<string>("hello", 4);
  p[5] = make_shared<string>("hello", 1, 2);
  p[6] = make_shared<string>(p[0]->rbegin(), p[0]->rend());
  p[7] = make_shared<string>(*p[0] + *p[1]);
  p[8] = make_shared<string>(string{'a', 'b', 'c'});
  for (int i = 0; i < 9; ++i) {
    cout << *p[i] << endl;
  }
  return 0;
}

输入

C风格

char info[100];
cin >> info;
cin.getline(info, 100); // read a line, discard \n.
cin.get(info, 100);     // read a line, leave \n in queue.

string风格

string stuff;
cin >> stuff;
getline(cin, stuff);// read a word, discard \n

getline(cin, stuff, ':') 可以指定分隔符号,并丢弃.

使用

  1. find(const string &str, size_type pos = 0) const;
  2. find(const char *s, size_type pos = 0) const;
  3. find(const char *s, size_type pos = 0, size_type n);
  4. find(const char ch, size_type pos = 0) const;
  5. rfind()
  6. find_first_of(const string &str, size_type pos = 0) const; // 匹配其中字符的首个位置
  7. find_last_of(const string &str, size_type pos = 0) const; // 匹配其中字符的最后一个位置
  8. find_first_not_of(const string &str, size_type pos = 0) const; // 匹配其中不包含在参数字符的首个位置

string的种类

typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string;
typedef basic_string<char32_t> u32string;

智能指针

  • auto_ptr(deprecated)
  • unique_ptr
  • shared_ptr

每个智能指针类都有一个explicit构造函数, 该构造函数将指针作为参数, 禁止了指针隐式转换(=).

注意: 不可以将智能指针指向非堆内存.

对于auto_ptr存在如下问题:

auto_ptr<string> ps (new string("I reigned lonely as a cloud."));
auto_ptr<string> vocation = ps; // not safe!

由于ps, vocation同时指向相同对象, 那么在智能指针析构的时会产生错误--同时删除一个对象两次.

解决方案:

  1. 重载赋值运算符, 执行深复制.
  2. 建立所有权概念, 赋值时转移所有权, 这就是unique_ptrauto_ptr.
  3. 创建更高级的智能指针, 跟踪引用特定对象的智能指针树. 称之为引用计数.当计数变为0, 删除之,
    这就是shared_ptr.

auto_ptr不会在编译时检查所有权转移后的非法调用这是与unique_ptr最明显的区别.

auto_ptr<string> p1(new string("auto"));
auto_ptr<string> p2 = p1; // p1的所有权被剥夺, p1将原则上不可被访问
unique_ptr<string> p3(new string("unique")); 
unique_ptr<string> p4 = p3; // 非法语句 无法通过编译

当程序试图将一个unique_ptr赋值给另一个, 仅当原变量是个临时变量才合法.
如下:

using namespace std;
unique_ptr<string> pu1(new string("hi ho!"));
unique_ptr<string> pu2;
pu2 = pu1;                                   // invalid
unique_ptr<string> pu3;
pu3 = unique_ptr<string>(new string("Yo!")); // valid

IO

  • cin/wcin
  • cout/wcout
  • cerr/wcerr
  • clog/wclog
int a;
cin >> hex;
// cin >> oct;
// cin >> dec;
cin >> a;

文件IO

注意ostream的open操作默认的模式是ios_base::out|ios_base::trunc, 默认截短清除文件内容.

使用is_open()来判断文件流是否正常关联.

  • seekg()和seekp()函数提供文件的随机存取。

    • istream & seekg(streamoff, ios_base::dir);

    • istream & seekg(streampos);

    • istream & seekp(streamoff, ios_base::dir);

    • istream & seekg(streampos);

      streampos, streamoff 的单位都是Byte.

  • tellg()和hellp()报告当前文件位置。
    g是输入流的指针
    p是输出流的指针

临时文件

<cstdio>中存在char *tmpnam(char * pszName)函数用于返回随机的文件名,常量L_tmpnam
TMP_MAX限制了文件明长度和不重复次数.

之后被mkstemp取代,相关函数:mktemp

头文件:#include <stdlib.h>

定义函数:int mkstemp(char * template);

函数说明:mkstemp()用来建立唯一的临时文件. 参数template 所指的文件名称字符串中
最后六个字符必须是XXXXXX.

Mkstemp()会以可读写模式和0600 权限来打开该文件, 如果该文件不存在则会建立该文件.

打开该文件后其文件描述词会返回. 文件顺利打开后返回可读写的文件描述词.
若果文件打开失败则返回NULL, 并把错误代码存在errno 中.

错误代码:EINVAL 参数template 字符串最后六个字符非XXXXXX. EEXIST 无法建立临时文件.

附加说明:

参数 template 所指的文件名称字符串必须声明为数组, 如:
char template[] = "template-XXXXXX";
千万不可以使用下列的表达方式
char *template = "template-XXXXXX";

内核格式化(incore formating)

读取string对象中的格式化信息或者将格式化信息写入string对象.

<sstream>文件中定义了ostringstreamistringstream.
例如

ostringstream outstr;
double price = 380.0;
char *ps = " for a copy of the ISO/EIC C++ standard!";
outstr.precision(2);
outstr << fixed;
outstr << "Pay only CHF" << price << ps << endl;
string mesg = outstr.str();

C+11 新特性

  1. 新内置类型

    long ling, unsigned long long, char16_tchar32_t.

  2. 统一初始化(花括号大一统)

    花括号可以用来初始化内置类型和用户定义类型(类) =可以省略.

    • 初始化列表 防止缩窄, 禁止数值赋予给无法存储它的数值变量.
    • initializer_list 作为参数, 使用迭代器遍历访问.
  3. 简化声明

    • atuo 自动类型推导

    • decltype 将变量的类型声明为表达式指定的类型

      double x;
      int n;
      decltype(x*n) q; // double
      decltype(&x) pd; // double *
      

      decltype类型推导可以为引用和const.

    • 返回类型后置

    double f1(double, int);
    auto f2(double,int) -> double; //useful for template
    
  4. 模板别名: using=
    不同于之前使用typedef, C+11提供了using=的语法.

    typedef std::vector<std::string>::iterator itType;
    using itType = std::vector<std::string>::iterator;
    

    差别在于新语法支持模板部分具体化

    template<typename T>
      using arr12 = std::array<T,12>;
    arr12<double> a1; // std::array<double,12> a1;
    
  5. nullptr
    确保0代表只为整型常量, nullptr为指针常量. 达到了更高的类型安全.

  6. 智能指针
    auto_ptr被摒弃, 新增三种:shared_ptr, unique_ptr 和weak_ptr

  7. 异常规范
    舍弃throw, 用noexcept代替.

  8. 作用域内枚举(枚举类)
    enum class New1{never, sometimes, often, always};
    enum class New2{never, lever, sever};
    使用时需要域描述符New1::never, New2::never.

  9. 对类的修改

    • explicit 禁止隐式转换
    • 类内成员初始化
  10. 模板和STL方面的修改

    • 基于范围的for循环
    • 新的STL容器
    • 新的STL方法
    • valarray升级
    • 摒弃export
    • 模板双尖括号间不要求空格
  11. 右值引用

    左值参数是可以被引用的数据对象, 如变量,数组元素, 结构成员, 引用和解引用的指针.
    非左值包括字面常量(字符串除外)和包含多项的表达式.

    • & 左值引用
    • && 右值引用
      std::move强制左值变右值或者static_cast<int &&>(i), 用于复制构造变移动构造.
  12. 特殊的成员函数

    除默认构造函数, 复制构造函数, 复制赋值运算符, 析构函数. 之后新增移动赋值运算符,
    移动复制函数.

  13. 默认方法和禁用方法

    • default显式地声明这些方法的默认版本. 只能用于6种特殊的成员函数
    • delete显式禁止编译器使用特定方法. 任意成员函数.
  14. 委托构造函数

    在构造函数的定义中使用另一个构造函数, 这被称为委托. (构造函数)

  15. 继承构造函数

    派生类可以继承基类构造函数的机制.

  16. 管理虚方法:override和final

    对于基类声明的虚方法, 派生类提供不同版本将会覆盖旧版本, 但是当特征不匹配, 将
    隐藏而不是覆盖旧版本.如下:

    class Action{
        int a;
    public:
        Action(int i = 0) : a(i){}
        int val() const {return a;}
        virtual void f(char ch) const {std::cout << val() << ch << "\n";}
    };
    class Bingo : public Action{
    public:
        Bingo(int i) : Action(i) {}
        virtual void f(char *ch) const {std::cout << val() << ch << "\n";}
    }
    

    这里f(char *ch)将会屏蔽f(char ch).可以用
    override指出要覆盖的一个虚函数(置于参数列表后).如果声明与基类方法不一致将会报错.
    final禁止派生类覆盖特定方法.

  17. Lambda表达式

    • 比较函数指针, 函数符和Lambda函数

      函数指针不允许内联优化

    • 匿名函数分配函数名可以重复使用

  18. 包装器

    • 包装器及模板的低效性

    • 修复问题
      利用模板function从调用特征(call signature)的角度定义了一个对象, 可用于包装调用
      特征相同的函数指针,函数对象, lambd表达式.

      std::function<double(char, int)> fdci;
      这样可以通过创建包装器来避免多次实例化模板函数, 模板函数接受包装器, 由包装器指定调用对象.

    • 其他方式

      • 临时对象

      • 形参与原始实参匹配

        template <typename T>
        T use_f(T v, std::function<T(t)>)f){
            return f(v);
        }
        
  19. 可变参数模板

    • 模板参数包

    • 函数参数包

    • unpack参数包

    • 递归

      template<typename... Args> // Args是一个模板参数包
      void show_list1(Args... args){} // args是一个函数参数包
      show_list1('S', 80, "sweet", 2.1);
      

      很显然Args与T的区别在于T只能匹配单个类型, 而Args可以匹配任意数目类型.
      为了避免无限递归, 考虑每次递归提取一个类型.

      template<typename... Args>
      void show_list3(T value, Args... args){
           std::cout << value << ", ";
           show_list3(args...);
      }
      
  20. 并行编程

    • thread_local
    • atomic
    • thread
    • mutex
    • condition_variable
    • future
  21. 新增库

    • <chrono>
    • <tuple> 任意多个不同类型的值
    • <ratio> 编译阶段的有理数算术库准确表示任何有理数.
    • <regex> TODO
  22. 低级编程

    • POD(plain old data) 宽松, 可以安全逐字节复制
    • 允许共用体的成员有构造函数和析构函数
    • 内存对齐信息获取alignof()
    • constexpr 编译阶段计算结果为常量的表达式, 让const变量存在只读内存中.
  23. 杂项

    • 依赖于实现的拓展整型(支持128位整数)
    • 字面量运算符 1000b
    • assert宏,引入static_assert用于编译时对断言进行测试.
    • 元编程的支持. TODO
posted @ 2021-10-05 18:31  司空亦墨  阅读(89)  评论(0编辑  收藏  举报