C++ Primer笔记

序:

  学习C++最重要的的边写边学。不理解一个语法知识的设计原因,就无法在脑海里形成一个逻辑链,只能死记硬背也注定记不深刻。
  所以我推荐初学者先阅读《Accelerated C++》一书。这本书是学习C++最好的入门书之一,通过一个个实例来让读者了解C++最常使用到的80%语法。剩下的20%可以通过《C++ Primer》来补充。我这次的笔记就是在记录那剩下的20%。

变量类型

理解变量类型
  • 类型修饰符(*或&)仅仅修饰单个变量
  • 从右向左阅读r的定义。离变量名最近的符号对变量的类型有最直接的影响。
int *p, q;   //p是int型指针,q是int
int *&r = p;  //r是对指针p的引用
int (*a)[n];  //数组指针:指向int数组的指针
int   *a[n]; //指针数组:[ ]的优先级高,a是一个数组,存放int*类型元素。
声明、定义、初始化
  • 变量能且只能被定义一次,但是可以被声明多次。
  • 值初始化是值使用了初始化器(即使用了圆括号或花括号)但却没有提供初始值的情况。
    • 注意,当不采用动态分配内存的方式(即不采用new运算符)时,写成int a();是错误的值初始化方式,因为这种方式声明了一个函数而不是进行值初始化。如果一定要进行值初始化,必须结合拷贝初始化使用,即写成int a=int();值初始化和默认初始化一样,对于内置类型初始化为0,对于类类型则调用其默认构造函数。
extern int i;           //声明i
int j;                  //声明并定义j
extern double pi=3.1416;//定义并初始化,此时虽然有extern语句,但是因为加了等号,所以成为定义
int *p=new int();       //值初始化
vector< string> vec(10);//值初始化
数组的特殊性
  • 数组不允许拷贝和赋值
  • 在很多用到数组名字的地方,编译器会自动把其替换成一个指向数组第一个元素的指针。使用auto时是这样,decltype时这种转变不会发生
int a[] = {0,1,2};
int a2[] = a;    //错误:不允许使用一个数组初始化另一个数组
a2 = a;          //错误:不允许把一个数组直接赋值给另一个数组
auto ia(a);      //ia是一个整型指针,指向a的第一个元素
decltype(a) ia3={1,2.3}; //decltype(a)返回的类型是由3个元素构成的数组
类型转换
  • 无符号类型注意不能赋值成负数。否则值实际为对这个负数取模。
  • 赋值给带符号类型一个超出它表示范围的值时,结果是未定义的。
  • 显式类型转换(cast)
    • static_cast:处理具有明确定义的类型转换,不包含底层const。,这种强制转换只会在编译时检查。 如果编译器检测到您尝试强制转换完全不兼容的类型,则static_cast会返回错误。
    • reinterpret:处理非关联的类型转换,通过改变对象的位模式。例如 pointer 和 int的无关类型的转换。
    • const_cast:只能改变运算对象的底层const,
    • dynamic_cast:在运行时检查基类指针和派生类指针之间的强制转换。

作用域

作用域嵌套
  • 内层作用域可以重定义外层作用域已有的名字。
  • 但可以通过作用域操作符来访问外层变量。::是作用域操作符。全局作用域没有名字,所以::name是特指全局作用域里的name变量。

const限定符

const基本对象
  • const对象一旦创建后其值就不能改变,所以const对象必须初始化
  • 默认情况下const对象仅在文件内有效,不同文件需要定义同名的const变量,它们是独立的。
  • 如果想只在一个文件内定义,可以使用extern关键字。定义语句前加extern关键字
//文件1中定义,该常量能被其他文件访问
extern cnost int bufSize = fcn();
//文件2中的声明,与文件1中是同一个
extern const int bufSize;
const与引用
  • 对const的引用,简称常量引用,即把引用绑定到const对象上。底层const
  • 想要引用常量必须使用常量引用,但常量引用可以引用非常量对象。
  • 允许为一个常量引用绑定非常量的对象、字面值,甚至一般表达式。而非常量引用是不可以的。
const与指针
  • 指向常量的指针:想要存放常量对象的地址,只能使用指向常量的指针。底层const
  • const指针:指针本身是const,即指针初始化后的值(那个地址)不能改变。顶层const
const int a;
const int &b =a;     //常量引用,底层const
int i = 42;
const int &r = i*42; //可以将const的引用绑定到右值上;

const int *p1 = &a;  //指向常量的指针,底层const
int *const p2 = &a;  //常量指针,p2永远指向a,顶层const

**在拷贝操作时,拷入和拷出的变量必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。
一般来说,非常量可以转换成常量,反之则不行**


异常处理

try语句块
  • throw表达式,可以用来引发异常。
  • try语句块处理异常,以try关键字开始,并以多个catch子句结束。
  • 异常类,用于在throw表达式和相关的catch子句之间传递异常的具体信息。

函数

尾置返回类型
  • 以auto开头,以->返回类型结尾。
int (*func(int i))[10];         //直接写法
auto func(int i) -> int(*)[10]; //尾置返回类型。返回一个指针,该指针指向含有10个整数的数组
typedef int arrT[10];          //arrT是一个类型别名
using arrT= int[10];            //arrT的等价声明
arrT* func(int i);              //使用类型别名。返回类型同上
局部静态对象
  • 局部静态对象在程序执行路径第一次经过对象定义语句时初始化,直到程序终止才被销毁。在此期间即使对象所在的函数结束执行,也不会对它造成影响。它的值可以改变。
含有可变形参的函数
  • initializer_list形参:这是在标准库中定义的一个模板类,其中的对象永远是const值无法改变。而且拷贝或赋值一个initializer_list对象使用的是传引用赋值。
  • 省略符形参:为了便于C++程序访问某些特殊的C代码而设置的,通常不用于其他目的。
    void error_msg(initializer_list<string> il);
    error_msg({"a","b"});
    error_msg({"a","b","c"});
    void foo(parm_list, ...); //逗号是可选的

数据成员和成员函数
  • 在类体内定义的成员函数默认为inline函数,在类体外定义的成员函数默认情况下不是内联的。
  • 类内初始值有限制:可以放在花括号里,或者等于号右边,但不能使用圆括号。
构造函数
  • 默认构造函数:如果存在类内初始值用它们初始化,否则默认初始化。按类中出现的顺序初始化。

    • 很多时候需要自己定义默认构造函数:
      1. 当类没有声明任何构造函数时,编译器才会自动生成默认构造函数。
      2. 如果定义在块中的内置类型或符复合类型(比如数组和指针)默认初始化,它们的值将是不确定的。
      3. 如果类内有其他类类型的成员,且这个成员的类型没有默认构造函数,那编译器将无法初始化该成员。
    • = default:在C++11里,如果需要默认行为,可以通过在参数列表后面加上 = default来要求编译器生成构造函数。Data() = default;
    • 当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。
  • 能通过一个实参调用的构造函数定义了一条从构造函数的参数类型向类类型隐式转换的规则。
  • 要意志隐式转换,可以使用explicit关键字。explicit关键字定义的构造函数可以用于显式地强制类型转换。
友元
  • 可以把非成员函数定义为友元,也可以把其他的类定义成友元,还可以把其他类(之前已定义)的成员函数定义为友元。
  • 友元不具有传递性,每个类负责控制自己的友元类或友元函数。
  • 把一组重载函数作为友元,需要对这组函数中的每一个进行声明。
静态成员
  • 通常类的静态成员不能在类的内部初始化。然而如果静态成员是constexper的就可以。
  • 静态成员能用于某些场景,而普通成员不能
    • 静态数据成员可以是不完全类型。
    • 可以使用静态数据成员作为成员函数的默认实参。

STL

泛型算法
  • 大多定义在头文件algorithm中,还有一部分在numeric中。
  • 因为泛型算法,只运行在迭代器之上,而不执行容器操作。所以泛型算法永远不会改变容器的大小。即只可能改变元素的值,或者移动元素,淡不会直接添加或者删除元素。使用插入迭代器可以改变容器大小,但这个和算法本身无关。
  • 只读算法
    • 通常最好使用cbegin()和cend()。但如果想用算法返回的迭代器来改变元素,就需要使用begin()和end()。
    • 那些只接受一个单一迭代器来表示第二个序列的算法,都假定第二个序列至少与第一个序列一样长。
    • 从两个序列读元素的算法,构成这两个序列的元素可以来自于不同类型的容器。如一个vector,一个list。
  • 写容器元素的算法
    • 向目的位置迭代器写如数据的算法假定目的位置足够大,能容纳要写入的元素。
  • lambda表达式:[捕获列表] (参数列表) -> 返回类型 { 函数体 }
    • 可以忽略参数列表和返回类型。但必须永远包括捕获列表和函数体。
    • 如果忽略返回类型,lambda根据函数体中的代码推断出返回类型。如果函数体只是一个return语句,则返回类型从返回表达式推断。否则返回类型为void。
    • 捕获值是在创建时拷贝,而不是调用时。
  • 参数绑定,bind函数:auto newCallable = bind(callable , arglist);
    • bind函数可以看成一个函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list参数。callable对象原有多少参数,arg_list就应该有多少个。

内存管理

1.静态内存:保存局部static对象、类static数据成员以及定义在任何函数之外的变量。static对象在使用前分配,在程序结束时销毁。
2.栈内存:保存定义在函数内的非static对象。栈对象仅在其定义的程序块运行时才存在。
3.动态内存:程序用堆来储存动态分配的对象,即那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说必须显式销毁。一般程序使用动态内存出于以下三种原因之一:
* 程序不知道自己需要使用多少对象
* 程序不知道所需对象的准确类型
* 程序需要在多个对象间共享数据

智能指针
  • shared_ptr:我们可以认为每个shared_ptr都有一个关联的计数器,通常称为引用计数。
    • 计数器递增:当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给函数,以及作为函数的返回值。
    • 计数器递减:当给shared_ptr赋一个新值,或者一个局部的shared_ptr离开其作用域。
    • shared_ptr在无用后任然保留的一种情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。这种情况应该用erase删除。
    • get使用:(1)确定代码不会delete指针的情况下,才使用get。(2)永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
  • unique_ptr:一个unique_ptr“拥有”它所指向的对象,所以不支持普通的拷贝和赋值操作。但可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr。
动态数组
int n=5;
int *pia = new int[n];//pia指向第一个int
delete [] pia;//pia必须指向一个动态数组或为空
allocator类
  • 标准库allocator类定义在memory中,它帮助我们将内存分配和对象构造分离开。

拷贝控制

拷贝、赋值、与销毁(三五法则)
  • 拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则为拷贝构造函数。
    • 一般此函数的第一个参数都是const的引用。
    • 因为在很多中情况下都会被隐式使用,所以通常不应该是explicit的。
    • 编译器可以略过对拷贝构造函数的调用,直接创建对象
    • 调用场景:
      • 用=定义变量时
      • 将一个对象作为实参传递给非引用类型的形参
      • 从一个返回类型为非引用类型的函数返回对象
      • 用花括号列表初始化一个数组中的元素或者一个聚合类的成员
      • 某些类类型还会对它们所分配的对象使用拷贝初始化,例如标准库容器使用insert或push成员
  • 赋值运算符
    • 名为operator=的函数。
    • 某些运算符,包括赋值运算符,必须定义为成员函数。
    • 赋值运算符通常应该返回一个指向其左侧运算对象的引用。
  • 析构函数
    • 名字由波浪号接类名组成,它没有返回值,也不接受参数。
    • 因为不接受参数,所以不能被重载,对一个给定的类只会有唯一一个析构函数。
    • 当指向一个对象的引用或指针离开作用域时,析构函数不会执行
    • 析构函数的函数体自身不直接销毁成员。成员是在析构函数体之后隐含的析构阶段中被销毁的。按初始化的逆序销毁。
    • 调用场景:只要当一个对象被销毁,就会自动调用其析构函数。
      • 变量在离开其作用域时被销毁。
      • 当一个对象被销毁时,其成员被销毁。
      • 容器(标准库容器和数组)被销毁时,其成员被销毁。
      • 对于动态分配的对象,当指向他的指针应用delete运算时被销毁。
      • 对于临时对象,当创建它的完整表达式结束时被销毁。
  • 注意事项
    • 需要析构函数的类也需要拷贝和赋值操作。需要拷贝操作的类也需要赋值操作,反之亦然。
    • 可以定义删除的函数来阻止拷贝:cpp a(const a&) = delete;
      • 析构函数通常不能是删除的。如果是,则不能定义该类型的变量,不能释放指向该类型的动态分配对象指针。
      • 合成的构造函数和拷贝控制成员可能是删除的。本质上是类中含有一些成员不能被默认构造、拷贝、赋值与销毁。
  • 右值引用:通过&&来获取,只能绑定到一个将要销毁的对象。
    • 右值:返回非引用类型的函数,连同算术、关系、位以及后置后置递增递减运算符,都会生成右值。
    • 可以将一个const的左值引用绑定到右值上。
    • 可以显示地将一个左值为对应的右值引用类型。也可以通过标准库move函数,来获得绑定到左值上的右值引用。
int &&rr1 = 42;             //ok
int &&rr2 = rr1;            //错误:表达式rr1是左值!
int a = 11;
int &ll1 = a;
int &&ll2 = static_cast< int&&>(ll1); //ok,显式转换
int &&rr3 = std::move(rr1); // ok
  • 移动构造函数:但类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个右值引用。
    • 不抛出异常的移动构造函数和移动赋值运算符必须标记为 noexcept。
    • 引用限定符:放成员函数参数列表后。用来指出this可以指向一个左值或右值。 P483
  • 移动赋值运算符
StrVec::StrVec(StrVec &&s) noexcept; //移动构造函数
StrVec &StrVec::operator=(StrVec &&rhs) noexcept; //移动赋值运算符
  • 三/五法则:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。

重载运算符与类型转换

重载
  • 通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。详细:选择作为成员还是非成员 P493。
  • 如果类同时定义了算数运算符和相关的复合赋值运算符,则通常情况下应该用符合赋值来实现算术运算符。
  • 区分前置和后置运算符
StrBlobPtr operator++();    //前置版本
StrBlobPtr operator++(int); //后置版本
函数对象
  • 如果类定义了调用运算符,则该类的对象称作函数对象
  • lambda是一种简便的函数对象。
  • 调用形式指明了调用返回的类型以及传递给调用的实参类型,如:int< int, int>。
  • 标准库function类型,可以用来保存一系列调用形式相同的可调用对象:function< int(int, int)>
类型转换符
  • 类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。
    cpp operator type() const;
  • 类型转换函数必须是成员函数,它不能声明返回类型,形参列表也必须为空。类型转换函数通常应该是const。
  • 编译器只能执行一个用户定义的类型转换,但是隐式的用于定义类类型转换可以置于一个标准类类型转换之前或者之后,并与其一起使用。
  • 显式的类型转换运算符:定义成显式的,将不能用于隐式类型转换。但存在一个例外:即如果表达式被用作条件,则编译器会显式的类型转换自动应用于它。
  • 类型转换的二义性。P519

面向对象程序设计

OOP面向对象编程
  • 核心思想:数据抽象、继承、动态绑定。
  • override和final关键字
    • override:使用override标记某个虚函数,如果该函数并没有覆盖已存在的虚函数,将报错
    • final:
      1.在类的后面跟final关键字,防止被继承。
      2.把某个函数定义成final,之后任何想覆盖该函数的操作都报错。
虚函数
  • 某个函数被声明为虚函数,则在所有派生类都是虚函数。
访问控制与继承
  • 访问权限
    • public:可以被任意实体访问
    • protected:只允许子类及本类的成员函数访问
    • private:只允许本类的成员函数访问。子类永远无法访问
  • 继承方式
    • public继承:基类的成员遵循原有的访问说明符。
    • pravate继承:所有成员都变为私有的。
    • protected继承:将基类中public成员变为子类的protected成员,其它成员的访问权限不变。
  • 对于代码中某个给定的节点而言,只有基类定义的公有成员是可访问的,则派生类向基类的类型转换也是可访问的。
  • 不能继承友元关系。
  • 可以用using声明改变基类中非私有成员的访问权限。using可以继承构造函数。
派生类的拷贝控制成员函数
  • 派生类必须使用基类的构造函数来初始化基类部分成员。
  • 当派生类定义了拷贝或移动操作时,该操作负责拷贝或移动包括基类部分成员在内的整个对象
  • 析构函数只负责销毁派生类自己分配的资源

模板与泛型编程

  • 非类型模板参数:表示一个值而不是一个类型;非类型模板参数的实参必须是常量表达式。
  • 保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
  • 令模板自己的类型参数称为友元。
  • 为模板类型定义别名。
  • 可以有默认模板实参
  • 成员模板:一个类可以包含模板成员函数。
  • 控制实例化:当模板被使用时才会实例化,当相同的实例出现在多个对象文件中,每个文件就会有一个该模板的实例,造成开销浪费。可以用extern来实例化声明,表达在程序的其他位置有一个定义。定义必须只有一个。
  • 模板特例化:在template后跟一个空尖括号,因为模板参数在函数参数中被指定为特定类型。本质是实例化一个模板。
template< unsigned N, unsigned M> //非类型模板参数

template< class Type> class Bar{
friend Type; //将访问权限授予用来实例化Bar的类型 Type
}

template< typename T> using twin = pair< T,T>;
twin< string> authors;    //authors是一个pair< string, string>

extern template class Blob< string>; //实例化声明
template class Blob< string>;        //实例化定义,在本文件中实例化。

命名空间

  • 命名空间可以不连续
  • 模板特例化必须定义在原始模板所属的命名空间中
  • 内联命名空间:其中的名字可以直接被外层命名空间使用。
  • 未命名的命名空间:关键字namespace后紧跟括号。未命名的命名空间中定义的变量拥有静态生命周期。

猜你喜欢

转载自blog.csdn.net/nikoong/article/details/80340484