C/C++教程

C++ -学习笔记 --《c++ primer plus》

本文主要是介绍C++ -学习笔记 --《c++ primer plus》,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

C++

1. C++基础知识

  • main()函数由启动代码调用,它是由编译器添加到程序中的。
  • C头文件被转换成C++头文件,去掉拓展名h,在开头加上c。代码加入了命名空间std用法。eg.math.h --> cmath
  • 使用endl可以刷新输出,而\n不能保证。使用dec、oct、hex来输出不同进制数据。
  • C++提供两种发送消息的方式,一种是使用类方法(函数调用),另一种是重定义运算符。
  • 函数原型只描述接口,即描述的是发送给函数的信息和返回的信息;函数定义包含了函数的代码。
  • using 编译指令、using声明指令,::为作用域解析运算符,尽量使用声明。
  • short至少16位;int至少与short一样长;long至少32位,且至少与int一样长;long long至少64位,且至少与long一样长。
  • 可以使用大括号初始化器来初始化常规变量和类变量,等号是可选项。e.g. int a{1}; ==> int a=1; C11
  • 强制类型转换方式
    1. (typename) value 来自C语言。
    2. typename(value) 纯粹的C++格式。
    3. 采用强制类型转换运算符,static_cast<typename> (value)
    4. reinterpret_cast是进行高风险转换,比如整型转换成指针,但不能将函数指针转换成数据指针。
    5. const_cast可以改变值为constvolatile,一般在需要删除const标签时使用。但原来的值并不能真正被修改,虽然都是指向相同的地址。
    6. dynamic_cast
  • c++包含两种类型:基本类型和复合类型。基本类型包含整数和浮点数;复合类型包含数组、字符串、指针、结构、共用体、枚举、类等。
  • cin读取一个单词,cin.getline()读取整行并删除换行符,cin.get()读取整行并保留换行符到输入序列。
  • 字符串字面量(字符串常量)
    1. char类型,8位
    2. wchar_t类型,L"xxxxx",16位~32位 经测试ubuntu为32位
    3. char16_t类型,u"xxxxx",16位 C++11
    4. char32_t类型,U"xxxxx",32位 C++11
    5. raw(原始)字符串,R"(ssdasad)",可与其他类型字符串结合使用,RU、Ru等等 C++11
    6. utf-8格式,u8"xxx",根据编码的数字值,可能存储为1-4个字节。
  • C++声明结构体时可以省略struct,C不可以。支持对象作为结构的成员变量(如string等(a))。
  • 枚举可以赋值负数,后面的仍然依次加一。
  • 计算机在存储数据时必须跟踪三种基本属性:地址、值、类型。
  • malloc相似,C++用new 用来申请内存,释放内存用deletedelete只会释放指针指向的内存,不会删除指针本身,即指针本身可以重复使用去指向另一个新分配的内存。只能用于释放new申请的内存,不要用于释放声明变量所获得的内存,也不要重复释放相同内存块。
  • vector模板类,是一种动态数组。array(C++11)模板类,与数组相同,是静态内存分配(放在栈中)。
  • 对于内置模式,前缀模式(如++a)与后缀模式没有什么差别。但对于自定义模式,因为要先复制一个副本将其加或减一,再返回结果,所以前缀模式的效率更高。
  • 用引号括起的字符串常量是其地址。
  • 文件输出/读取主要步骤:
    1. 包含头文件fstream
    2. 创建一个ofstream或ifstream
    3. 将该对象与文件关联起来。
    4. 像使用cout或cin那样使用它。
  • 内联函数是将相应的函数代码替换函数调用,比常规函数梢快,但需要占用更多内存。主要节省了函数调用机制的时间。需要在函数声明、定义前加inline。一般直接在声明出直接定义。不可以递归,否则编译器会认为常规函数。
  • 引用变量,是已定义变量的一个别名,用&定义。指向相同的值和内存单元。必须声明时初始化。接近const指针。
  • 函数使用引用参数(必须是变量,不能是表达式,加上const可以生成临时变量再引用,此时可以转换类型),允许被调函数访问主调函数的变量。当然用指针也可以实现。节约时间和内存。
  • 函数模板使用泛型来定义函数,其中的泛型可用具体的类型替换。通过将类型作为参数传递给模板,可使编译器生成该类型的模板。可用typenameclass来声明参数类型。
  • 具体化函数优先于常规模板函数,具体化包括:隐式实例化、显式实例化、显式具体化。是处理具体类型的函数。template<>为显式具体化、template为显式实例化。
  • decltype用在函数模板中,提供参数的类型。autodecltype结合使用,定义模板函数。C++11
  • 定位new运算符,可以指定要使用的位置。需要包含new头文件。int *p = new (buffer) int;

2.存储持续性、作用域和链接性

  • 存储持续性(c中称存储类别),c++使用四种同的方案来存储数据,区别在于数据保留在内存中的时间
    1. 自动存储持续性(期),执行函数、代码块时被创建,执行完毕就释放。
    2. 静态存储持续性(期),在整个程序运行过程中都存在。
    3. 线程存储持续性(期)(C++11),与对应线程生命周期相同。
    4. 动态存储持续性(期),用new等关键字自己分配的,直到自己delete或者程序结束。
  • 作用域描述了名称在文件的多大范围内可见。
  • 链接描述了名称如何在不同单元之间共享。

3. 对象和类

  • 类对象默认访问控制为private,所以可以不写private。
  • 定义成员函数时,要使用作用域解析运算符来标识函数所属的类。
  • 定义位于类声明中的函数都将自动成为内联函数,声明外的成员函数加上inline也可以成为内联函数。
  • 每个对象都有自己的存储空间,但是同一个类的所有对象共享同一组方法,即每个方法只有一个副本。
  • 要创建类对象,可以声明类变量(显式构造、隐式构造),也可以使用new为对象分配存储空间。没有提供构造函数时,编译器会提供默认构造函数,若提供了非默认构造函数,则使用自定义构造函数。可以用大括号初始化C++11
  • 对象的的生命周期与该对象的存储持续性相同,对象释放时将调用析构函数。
  • 可以在内部方法后面加上const关键字,表示类方法不会修改调用对象,称为const成员函数。不是类成员函数不允许在后面加const
  • this指针指向调用成员函数的对象(被作为隐藏参数传递给方法),注意this是指针*this是该对象,与java不同。每个成员函数都有一个this指针。
  • 可以使用枚举或者关键字static const来创建一个所有对象共享的常量。
  • 可以使用operatorop()来重载运算符,op是运算符。如operator*(),*将被重载。掉用方法时可以使用运算符表示法,符号左侧是调用对象,右侧是参数。也可以使用常规的形式调用方法。
  • 重载运算符的限制:
    1. 重载的运算符必须至少有一个操作数是用户定义的类型。
    2. 使用运算符时不能违反原来的句法规则,不能修改运算符的优先级。
    3. 不能创建新的运算符。
    4. 不能重载规定外的运算符。通过成员函数或非成员函数进行重载,非成员函数比成员函数多一个参数,也不能同时定义。
  • 友元函数,通过让函数(函数原型添加friend关键字)成为类的友元,可以赋予该函数与类成员函数相同的访问权限,如访问私有变量。
  • 类的自动转换,当构造参数只有一个,或者其他参数拥有默认值时,可以直接用=进行隐式的转换,当在构造函数声明前面加上explicit时将屏蔽这种隐式转换,此时值能进行显式转换。也就是利用构造函数可以将某种类型转换成类类型。
  • 转换函数与构造函数的转换相反,是将类对象转换成所需要的类型。实现类方法 operator typename()即可自动调用。C++11中可以加上explicit关键字,将不会隐式转换,只能显式转换(注意C++98中不允许explicit加在函数前面)。explicit只能处于类函数原型中,定义时不加。
    1. 转换函数必须是类方法。
    2. 转换方法不能声明返回类型,但是可以返回所需的值。
    3. 转换函数不能有参数。
  • 静态类成员的特点(static 修饰),所有对象都将只创建一个静态类变量副本。不能在类声明中初始化静态成员变量。当静态数据成员为const整数类型或者枚举型时可以在声明时初始化。
  • C++会自动为类提供以下成员函数(如果没有定义),可以使用伪私有方法可以避免。
    1. 默认构造函数,没有任何参数,也不执行任何操作,若定义了构造函数则不会提供默认构造函数。
    2. 默认析构函数
    3. 默认复制构造函数,当新建一个对象并将其初始化成同类的现有对象时,会逐个赋值非静态成员(成员复制也叫浅复制),按值传递和返回对象时都会调用,静态数据不受影响。
    4. 赋值运算符,自动为类重载赋值运算符,也是浅复制,静态数据不受影响。
    5. 地址运算符
  • 注意不要将赋值与初始化搞错了,如果是创建新对象则使用初始化;如果是修改已有对象的值,则是赋值。
  • 深度复制,如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针。
  • 浅复制或成员复制,只是复制指针值,不会复制指向的数据。
  • C++11提供新关键字nullptr来表示空指针。
  • 静态的类成员函数,只能使用静态数据成员,不能通过对象进行调用,需要用类名加上作用域解析运算符来调用。如String::HowMany()
  • 需要显式的为定位new运算符创建的对象调用析构函数,且应以创建顺序相反的顺序进行删除,因为晚创建的对象可能依赖于早创建的对象,当所有对象都被销毁后才能释放存储这些对象的缓冲区。
  • const常量数据和被声明为引用的类成员初始化时,需要在构造函数右边进行初始化,这会在创建对象前初始化,其他参数也可以。不过成员初始化列表语法只能用于构造函数。
  • C++11允许在类声明处初始化成员变量。

4. 类的继承

  • 继承类(也称派生类,java称子类)不能直接访问基类(java称父类)的私有成员,而必须通过基类方法进行访问。也就是说派生类构造函数必须使用基类的构造函数。可以通过作用域解析运算符调用基类方法,若派生类没有重新定义该方法,则可以不用作用域解析运算符直接调用。
  • 创建派生类对象时,会先创建基类的对象,可以使用成员初始化列表语法来完成。派生类对象过期时,先调用派生类析构函数,再调用基类析构函数。
  • 基类指针或引用可指向派生类(反之不可以),但也只能调用基类的方法。
  • 多态的实现,通常使用虚方法,程序会根据对象类型而不是引用或者指针的类型来选择方法版本。为基类声明一个需析构函数也是一种惯例。关键字virtual只用于函数原型,不用于定义中。
  • 虚函数由编译器实现,会运用即可,不过一种实现方法是给每个对象隐藏添加了一个指向函数地址数组的指针,称为虚函数表。
  • 虚函数注意事项:
    1. 构造函数不能是虚函数。
    2. 基类析构函数应当是虚函数,这样可以通过指向对象的基类指针或者引用来派生对象时,会先调用派生类的析构函数,然后调用基类的析构函数,而不仅仅调用基类的析构函数。
    3. 友元函数不能是虚函数,因为成员函数才能是虚函数。
    4. 派生类没有重新定义,将使用基类版本。
    5. 若重新定义集成的方法,则应该与原来的函数原型一致,但如果返回类型是基类引用或者指针,则可以修改为派生类引用或者指针。如果基类声明被重载了,应该重新定义所有基类版本,若不重新定义,则派生类将无法使用该函数。(由虚函数实现的按对象类型调用也会失效)
  • 纯虚函数的结尾处=0,当类声明中存在纯虚函数时,则不能创建该类的对象。纯虚函数可以定义也可以不定义。
  • 抽象类至少存在一个纯虚函数。
  • 当派生类没有使用动态分配new时,默认的构造、复制、赋值、析构函数是能正常处理的。
  • 当派生类使用new时,将按下面操作自行定义:
    1. 派生类析构函数自动调用基类析构函数,只需要处理自己的部分数据就行。
    2. 复制构造函数,需要派生类调用基类的复制构造函数。
    3. 赋值运算符,需要派生类显式调用基类赋值运算符。
  • 友元函数不是成员函数,派生类不会继承,需要自己重新编写。
  • 应该尽量按照按引用传递对象,节约资源,且在继承使用虚方法时,被定义为接受基类引用参数可以接受派生类对象。
  • 尽量返回引用,但不可以返回临时对象的引用。由于返回引用的函数放在赋值语句的左侧,因此可以加上const来避免被修改。
  • 参数使用const可以确保不修改参数,放在后面可以确保不修改它的对象。

5. C++中的代码重用

  • 通常在单参数的构造函数前加上explicit关键字,防止隐式转换。
  • 私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。使用private进行继承(默认)。
    • 私有继承中,未进行显式类型转换的派生类引用或者指针,无法赋值给基类的引用或指针。即不能向上隐式转换。
  • 保护继承,基类公有成员和保护成员都将成为派生类的保护成员。使用protected进行继承。
  • 对于私有继承、保护继承,可以使用using重新定义访问权限。
  • 多重继承,在祖先相同时必须使用虚基类,并修改构造函数初始化列表的规则,通过优先规则解决名称歧义性。
    1. 使用基类指针来引用派生类对象时,若不是虚基类,则需要显式强制转换。
    2. 使用虚基类将使得从多个类(基类相同)派生出的对象只继承一个基类对象。继承时使用virtual关键字。
    3. 对于构造函数,使用虚基类时,禁止信息通过中间类自动传递给基类,基类会使用默认构造函数。如果不希望默认构造函数创建虚基类对象,需要显示的调用所需的基类构造函数。
    4. 如果从两个不同类继承了两个同名成员,应该使用::来区分。若使用虚基类,该方法不能叠加。
    5. 派生类中的名称优先于直接或者间接祖先类中的相同名称。
  • 类模板,将类型名作为参数传递给接收方来建立类(或者函数,就是函数模板)。类开头加上template<class Type>,表示是模板类;加在成员函数上方,表示是函数模板。可以用作基类。
  • 可以加入非类型参数如template<class Type,int n>class等价于typename,表达式参数可以是整型、枚举、引用或指针,另外用作表达式参数n的值必须是常量表达式。使用表达式参数的方法的缺点主要是,每次改变n的值都会生成自己的模板,因此使用构造参数更通用。
  • 与函数模板相同,有以下方式,统称为具体化,如果有多个模板可供选择,则选择具体化程度最高的。
    1. 隐式实例化
    2. 显式实例化,template class Classname<T>;
    3. 显式具体化,template <> class Classname<specialized-tyep-name>{...};,当具体化模板与通用模板都与实例化请求匹配时,使用具体化版本。目的是对模板进行修改,使其行为不同。
    4. 部分具体化,该特性使得能够对类型参数设置各种限制,部分限制模板通用性。
  • 模板可用作结构、类和模板类的成员。在类中加入模板类成员,若编译器支持可以采用类外定义,如下:
//采用嵌套的方式
template<typename T>
	template<typename V>
  • 模板可以用作参数,模板可以包含本身就是模板的模板参数,如:
template<template<typename T> class Thing>
class Crab{
}

其中template<typename T> class是类型,Thing是参数,类中可以使用Thing<int>等作为模板参数的实例化。

  • 模板类也可以有友元函数,包括非模板友元、约束模板友元、非约束模板友元。
  • 可用typdef xx newnameusing newname = xx来设定别名。

6. 友元、异常和其他

  • 友元类,友元类的所有方法都可以访问原始类的私有成员和保护乘员。friend class classname可以放在公有、私有、保护位置,友元类的声明与位置无关。
  • 友元成员函数,仅将特定类成员成为另一个类的友元。需要使用前向声明。
  • 其他友元形式:相互互为友元类、一个函数是另外类的共同友元函数或者类。
  • 嵌套类,在另一个类中声明的类。通常是帮助实现另一个类,并避免名称冲突。
  • void onoff(){state=(state==on)? Off : On;},若两种状态分别为true(1)和false(0),则可以用异或和赋值运算符简写,void onoff(){state ^= 1;}
  • 异常代码块try{...}catch( ){...}catch可以捕获字符串,但通常是类类型(使用类引用)。
  • 异常规范(C++98,C++11废弃但支持),它可能出现在函数原型和函数定义中(void test(int a) throw()),作用之一是告诉用户可能需要使用try块,另一作用是让编译器添加运行阶段检查代码,检查是否违反了异常规范。noexcept指出函数不会发生异常(C++11)。
  • 若发生未捕获异常,将默认情况下调用terminate()terminate()再调用abort();若发生异常规范中没有的异常(也没有std::bad_exception类型,否则会被改该类型替代),则调用unexpected(),它会去调用terminate()。若不想程序终止,则应该重写上述方法。
  • exception异常类在头文件exception中,可以用它作为其他异常类的基类,what()虚函数可以返回一个字符串。
  • 头文件stdexcept中定义了一些异常类,如logic_error等。通过catch不同的类名,可以实现分别处理异常;在具备继承关系的异常类中,可以实现一起处理。(要求catch块从派生类开始,一直到基类,再到上一层的基类的顺序排放)
  • bad_alloc异常可用于捕获new分配时的异常,头文件new中包含了该类的声明。由于之前的new是返回空指针,所以C++提供了一个标志,让用户选择。int *pi = new (std::nothrow) int;int *pa = new (std::nowthrow) int[500];
  • 若使用栈区内存(自动管理),通过异常提前结束了函数,该内存也能被正确释放(栈解退);但若使用newmalloc等关键字(堆内存管理),提前出现异常将导致内存泄漏(需要在catch中进行清理内存操作,或者使用智能指针模板)。
  • RTTI(运行阶段类型识别)
    1. dynamic_cast运算符,可以回答“是否可以安全的将对象的地址赋值给特定类型的指针”这样的问题。dynamic_cast<type *>(pt),如果可以将对象地址转换成指向type的地址,则返回该,否则返回空指针。也可以用于引用,当无法强制转换时,将引发bad_cast异常,需要头文件typeinfo
    2. typeid运算符,返回一个type_info对象。
    3. type_info类,在头文件typeinfo中,其成员函数name()将返回该类的名称。该类重载了==!=,所以可以确定两个对象是否为相同类型。

7. string类和标准模板库

  • string构造函数,7个。输入方法2种。
  • 智能指针,将申请的内存地址放在一个指针对象中,并将释放该内存的方法,放在该指针类的析构函数中。有三个智能指针模板:auto_ptr(C++98提出,C++11已经废弃),unique_ptr(C++11),shared_ptr(C++11)。必须包含头文件memory,位于名称空间std中。不可以用于堆内存。
//auto_ptr模板函数如下
template<class X> class auto_ptr{
public:
	explicit auto_ptr(X *p=0) throw();
	...
}
//使用
auto_ptr<double> pdu(new double);//获得一个指向double的auto_ptr
  • 防止智能指针delete两次对象的方法
    1. 定义赋值运算符,使之深复制。
    2. 建立所有权概念,对于特定的对象,只有一个智能指针能拥有它,auto_ptrunique_ptr采用这种方法。
    3. 创建更高智能指针,跟踪引用特定对象的智能指针数,到0时才deleteshared_ptr采用这个方法。
  • auto_ptr在将指针所有权给别人后,不可以再使用该指针。
  • unique_ptr会在编译阶段控制这种错误(auto_ptr中的错误),但当unique_ptr是一个临时右值时编译器不会报错,因为不会产生悬挂指针。使用std::move()可以将一个unique_ptr赋值给另一个unique_ptr。当使用new[]分配内存时,只能使用unique_ptr.
  • 当程序要使用多个指针指向对象时,应该选择shared_ptr
  • STL(标准模板库),提供了一组表示容器、迭代器、函数对象和算法的模板。
  • 迭代器是一个广义的指针,可以对其执行类似的如解除引用(operater*())和递增(operater++())的对象。
vector<double> scores;
vector<double>::iterator pd=scores.begin();	//迭代器类似于指针
*pd=22.3;
pd++;
  • 泛型编程,关注的是算法,旨在编写独立于数据类型的代码。在C++中使用模板来实现按泛型定义的函数或类,而STL通过通用算法而更近了一步。
  • 模板使得算法独立于存储的数据结构,而迭代器使算法独立于使用的容器类型。
  • STL中有五种迭代器类型,分别为输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。
  • C++将operator++()作为前缀版本,operator++(int)作为后缀版本。
  • 只要提供适当的迭代器(可以是指针,也可以是对象)和超尾指示器,就可以将STL算法用于自己设计的数组形式。
  • 函数对象也称函数符,是可以以函数方式与()结合使用的任意对象。

课后练习题答案

这篇关于C++ -学习笔记 --《c++ primer plus》的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!