基本类型
- 注意事项
- 当明知道数值不可能为负时,选用无符号类型
- 在算数表达式中尽量不要使用char
因为char在有的机器上是有符号的, 但有的机器上是无符号的
如果要使用,必须明确指定signed char还是unsigned char
切勿混用带符号类型和无符号类型
当带符号类型取值变为负数时,会出现异常结果 这是因为带符号数会自动转换成为无符号数
#include <iostream>
int main(){
// 切勿混用带符号类型和无符号类型.
// 当带符号类型取值变为负数时,会出现异常结果
// 这是因为带符号数会自动转换成为无符号数
unsigned a = 10, b =42;
int c = 10, d = 42;
std::cout<<c-a<<std::endl; //0
std::cout<<a-c<<std::endl; //0
std::cout<<d-c<<std::endl; //32
std::cout<<a-b<<std::endl; //4294967264
std::cout<<b-c<<std::endl; //32
std::cout<<c-b<<std::endl; //4294967264
}
变量
- 初始化和赋值
初始化不是赋值
初始化: 创建变量时赋予其一个初始值
赋值:将当前的值擦掉, 赋予起新值
未初始化变量,常常会引发故障,所以记得初始化.
- 变量的声明(declaration)和定义(definition)
C++将变量的声明和定义区分开来
声明:使得名字为程序所知, 如果一个程序想要使用别处的变量,则必须对其进行声明
声明规定了变量的类型和名字
定义:负责创建与名字相关联的实体
除此之外,还申请储存空间,还可能为其赋上初始值
如果想声明一个变量,而不对其进行定义,则使用extern
extern int i; //声明i,而不进行定义
int j; //声明并定义j
extern k = 1; //声明并定义j
变量的定义只能出现在一个文件当中,其他要使用该变量的文件,只能对其进行声明,而不能进行定义.
在函数内部,试图初始化一个由extern标记的变量,会产生错误.
2.3复合类型
引用
reference引用实际是“左值引用”。實際上就是爲對象起了一個新的名字。
一般初始化變量時,初始值會被拷貝到新建的對象中,然而定義引用時,程序把引用和他的初始值綁定在了一起,而不是把初始值拷貝給引用。引用必須初始化。
int ab = 10;
int& c = ab;
c = 22;
std::cout << c << std::endl; // 22
std::cout << ab << std::endl; // 22
int d = 20;
c = d;
std::cout << c << std::endl; //20
與指針的區別
- 是否可变:指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。
- 是否占内存:指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间
- 是否可为空:指针可以为空,但是引用必须绑定对象
- 是否能为多级:指针可以有多级,但是引用只能一级
const限定符
const对象一旦创建后,就不能改变了,所以const对象必须初始化。
- const的引用
const int ci = 102;
const int& r = ci;
int& t = ci; // 错误,必须使用const引用
- 指针和const
指针本身是一个对象,他又可以指向另一个对象。所以指针本身是否为常量,及指针所指的对象是否为常量是两个问题,所以这里就有了顶层cosnt: 表示指针本身就是个常量。底层cosnt: 表示指针所指的对象是一个常量。
int i = 0;
int *const p1 = &i; // 顶层const, p1不能改变
const int ci = 42; // 顶层const, ci不能改变
const int *p2 = &ci; // 底层const, p2允许改变
const int *const p3 = p2; // 左顶层引用,右底层引用
constexpr
const expression常量表达式是指不会改变且在编译过程就能得到计算结果的表达式。
const int max_f = 20; // 常量表达式
const int limit = max_f + 1; // 常量表达式
int kof = 28; // 不是常量表达式, kof 为变量
const int sz = get_size() // 不是常量表达式, sz的具体值只有等到运行时才知道
优化编译过程: constexpr可以用来修饰变量、函数、构造函数。一旦以上任何元素被constexpr修饰,那么等于说是告诉编译器 “请大胆地将我看成编译时就能得出常量值的表达式去优化我”。
程序员告诉编译器尽管信心十足地把func当做是编译期就能计算出值的程式,但却欺骗了它,程序员最终并没有传递一个常量字面值到该函数。没有被编译器中止编译并报错的原因在于编译器并没有100%相信程序员,当其检测到func的参数是一个常量字面值的时候,编译器才会去对其做优化,否则,依然会将计算任务留给运行时。
constexpr int func(const int n){
return 10+n;
}
main(){
const int i = cin.get();
cout<<func(i);
}
//编译通过
类型别名
相当于给类型取个别名,易于理解使用
- typedef
typedef double wages; // wages等同于double
typedef wages base, *p // p是double*的同义词
- using
using SI = Sales_item // 等同 typedef Sales_item SI;
using wages = double;
auto
auto可以通过让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须要有初始值。
auto会忽略掉顶层的const, 但会保留底层的const。
如果是引用类型,auto则会使用引用对象的类型作为auto的类型。
auto a = 3.15;
int b = 9;
auto c = b+9;
int &bb = a;
auto cc = bb; // cc 为 int
// 引用也同样适用
auto &d = c;
const int j = 9;
// 忽略掉了顶层的const
auto k = j; // k是int
const auto h = j; // h 是const int
auto e = &j; // e是指向整型常量的指针,底层const保留
decltype
只想使用其数据类型,但并不想使用该表达式的初始值。
处理顶层const和引用方式与auto不同。
decltype会返回该变量类型的顶层const和引用
decltype(())双层括号,的结果永远是引用
cosnt int ci = 0, &cj = ci;
int i = 100, &r = i;
decltype(ci) x = 0; // const int
decltype(cj) y = x; // const int&, y绑定到x
decltype(cj) z; // 错误,引用未初始化
decltype(i) j; // 未初始化的int
decltype(r+0) j; // 未初始化的int
decltype(r) j; // 错误,引用未初始化
decltype((i)) j; // 错误,引用未初始化,decltype(())双层括号,的结果永远是引用
名字的作用域
c++中的作用域以花括号{}分隔
全局作用域
块作用域
注意:如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量
int reused = 50;
int main(){
int unique = 0;
std::cout<< unique << " " << reused <<std::endl; // 0 50
int reused = 30;
std::cout<< unique << " " << reused <<std::endl; // 0 30
// ::reused显示的访问全局作用域
std::cout<< unique << " " << ::reused <<std::endl; // 0 50
}
2.6 自定义数据结构
2.6.3 编写自己的头文件
类通常被定义在头文件中,类名一般与头文件名一样。为了头文件多次包含能能正常工作,必须进行预处理。
使用头文件保护符
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
....
#endif
第一次包含头文件时,SALES_DATA_H未定义,#ifndef判断为true,这时定义SALES_DATA_H,直到运行到#endif结束。
后面多次包含头文件时,SALES_DATA_H已经被定义,#ifndef判断为false,这时,直接跳转到#endif。中间过程都被忽略。避免了multiple definition.
通常#define xxx,xxx为文件名,全都大写,这样能保证唯一性和避免与程序中的其他实体发生名字冲突。
CH.4 表达式
4.11 类型装换
显示装换
static_cast
- 其最大的作用是能够用于基础数据上
float floatValue = 21.7;
static_cast<int>(floatValue);
- 转换必须是要有关联的: 用于类层次之间的基类和派生类之间 指针或者引用 的转换(不要求必须包含虚函数,但必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。
- 可以将空指针转化成目标类型的空指针
- 可以将任何类型的表达式转化成 void 类型
reinterpret_cast
reference
reinterpret_cast运算符是用来处理无关类型之间的转换;它会产生一个新的值,这个值会有与原始参数(expressoin)有完全相同的比特位.
- 改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型
const_cast
reference
const_cast主要用来来去除const限定。(注意:表达式的类型和要转化的类型是相同的)
const_cast<pointer>(val);
const char* pc;
const_cast<string>(pc); // 错误: 表达式的类型和要转化的类型是相同的
const_cast<char*>(pc); // 正确
如果修改使用去除const后的变量,原const值也不会变
const int constant = 21;
const int* const_p = &constant;
int* modifier = const_cast<int*>(const_p);
*modifier = 7;
//如果我们把结果打印出来:
cout << "constant: "<< constant <<endl;
cout << "const_p: "<< *const_p <<endl;
cout << "modifier: "<< *modifier <<endl;
/**
constant: 21
const_p: 7
modifier: 7
**/
//constant还是保留了它原来的值。
//可是它们的确指向了同一个地址呀:
cout << "constant: "<< &constant <<endl;
cout << "const_p: "<< const_p <<endl;
cout << "modifier: "<< modifier <<endl;
/**
constant: 0x7fff5fbff72c
const_p: 0x7fff5fbff72c
modifier: 0x7fff5fbff72c
**/
*modifier = 7;”为“未定义行为(Undefined Behavior)”。所谓未定义,是说这个语句在标准C++中没有明确的规定,由编译器来决定如何处理。应该避免这种情况,不要修改const限定的值。
为什么使用const_cast限定?
const限定的变量,我们不应修改其值。
我们可能调用了一个参数不是const的函数,而我们要传进去的实际参数确实const的,但是我们知道这个函数是不会对参数做修改的。于是我们就需要使用const_cast去除const限定,以便函数能够接受这个实际参数。
#include <iostream>
using namespace std;
void Printer (int* val,string seperator = "\n")
{
cout << val<< seperator;
}
int main(void)
{
const int consatant = 20;
//Printer(consatant);//Error: invalid conversion from 'int' to 'int*'
Printer(const_cast<int *>(&consatant));
return 0;
}
dynamic_cast
- 其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。
- 只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回 NULL;不能用于基本数据类型的转换。
- 在向上进行转换(永远安全)时,即派生类类的指针转换成基类类的指针和 static_cast 效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)。
#include <iostream>
#include <cstring>
using namespace std;
class Base
{
};
class Derive : public Base
{
};
int main()
{
Base *p1 = new Derive();
Derive *p2 = new Derive();
//向上类型转换
// 仅仅指针变了,对象未变
p1 = dynamic_cast<Base *>(p2);
if (p1 == NULL)
{
cout << "NULL" << endl;
}
else
{
cout << "NOT NULL" << endl; //输出
}
return 0;
}
- 在下行转换时,基类的指针类型转化为派生类类的指针类型,只有当要转换的指针指向的对象类型和转化以后的对象类型相同时,才会转化成功。
#include <cstring>
#include <iostream>
using namespace std;
class Base {
public:
virtual void fun() { cout << "Base::fun()" << endl; }
};
class Derive : public Base {
public:
virtual void fun() { cout << "Derive::fun()" << endl; }
};
int main() {
Base *p1 = new Derive();
Base *p2 = new Base();
Derive *p3 = new Derive();
//转换成功
p1->fun(); // Derive::fun()
p3 = dynamic_cast<Derive *>(p1);
if (p3 == NULL) {
cout << "NULL" << endl;
} else {
cout << "NOT NULL" << endl; // 输出
}
p3->fun(); // Derive::fun()
// p1, p3实际上对象类型相同,都是Derive
//转换失败
p3 = dynamic_cast<Derive *>(p2);
if (p3 == NULL) {
cout << "NULL" << endl; // 输出
} else {
cout << "NOT NULL" << endl;
}
p2->fun(); // Base::fun()
// p2的对象为Base, p3的对象为Derive, 对象类型不一样所以转换失败
Derive *p4 = new Derive();
// 向上装换
Base *p5 = dynamic_cast<Base *>(p4);
if (p5 == NULL) {
cout << " p4 NULL" << endl; // 输出
} else {
cout << "NOT NULL" << endl;
}
p5->fun(); // Derive::fun()
return 0;
}
define和typedef
- #define 作为预处理指令,在编译预处理时进行替换操作,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。typedef 是关键字,在编译时处理,有类型检查功能,用来给一个已经存在的类型一个别名,但不能在一个函数定义里面使用 typedef 。
- 功能:typedef 用来定义类型的别名,方便使用。#define 不仅可以为类型取别名,还可以定义常量、变量、编译开关等。
- 作用域:#define 没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef 有自己的作用域。
- 指针的操作:typedef 和 #define 在处理指针时不完全一样
#include <iostream>
// 注意define后没有;
#define INTPTR1 int *
typedef int * INTPTR2;
using namespace std;
int main()
{
// define相当于直接复制展开
INTPTR1 p1, p2; // p1: int *; p2: int
//等效于 int * p1, p2;
INTPTR2 p3, p4; // p3: int *; p4: int *
int var = 1;
const INTPTR1 p5 = &var; // 相当于 const int * p5; 常量指针,即不可以通过 p5 去修改 p5 指向的内容,但是 p5 可以指向其他内容。
const INTPTR2 p6 = &var; // 相当于 int * const p6; 指针常量,不可使 p6 再指向其他内容。
return 0;
}
CH.12 动态内存
12.1 智能指针
为什么要使用智能指针
智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针是一个类,当超出了类的实例对象的作用域时,会自动调用对象的析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。
用法
make_shared<T>(args); // 返回一个shared_ptr,指向动态分配内存的T类型对象,初始值为args.
shared_ptr<T>p(q); // p是shared_ptr q的拷贝,q中的计数器会加一。q中的指针必须能够转换为T*,
// q如果为普通指针,会产生危险,应为q的计数器不会增加,当p销毁时,q也会被销毁。
p = q; // p,q都为shared_ptr, p的计数器递减,q的计数器递增,当p为0时,p管理的内存释放
p.unique() // true,如果p.use_count()为1; 否则为false
p.use_count() // 返回p共享对象的智能指针数
p.reset(); // 悬空p指针,如果p是唯一指向对象的shared_ptr,则释放对象
引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。还有一个区别是std::shared_ptr不能用于管理C语言风格的动态数组,这点要注意。下面看例子:
int main()
{
auto ptr1 = std::make_shared<Resource>();
cout << ptr1.use_count() << endl; // output: 1
{
auto ptr2 = ptr1; // 通过复制构造函数使两个对象管理同一块内存
shared_ptr<Resource> ptr4(ptr1); // 拷贝ptr1到ptr4
std::shared_ptr<Resource> ptr3; // 初始化为空
ptr3 = ptr1; // 通过赋值,共享内存
cout << ptr1.use_count() << endl; // output: 4
cout << ptr2.use_count() << endl; // output: 4
cout << ptr3.use_count() << endl; // output: 4
cout << ptr4.use_count() << endl; // output: 4
}
// 此时ptr2与ptr3对象析构了
cout << ptr1.use_count() << endl; // output: 1
cin.ignore(10);
return 0;
}
可以看到,通过复制构造函数或者赋值来共享内存,知道这一点很重要,看下面的例子:
int main()
{
Resource* res = new Resource;
std::shared_ptr<Resource> ptr1{ res };
cout << ptr1.use_count() << endl; // output: 1
{
std::shared_ptr<Resource> ptr2{ res }; // 用同一块内存初始化
cout << ptr1.use_count() << endl; // output: 1
cout << ptr2.use_count() << endl; // output: 1
}
// 此时ptr2ptr3对象析构了, output:Resource destroyed
cout << ptr1.use_count() << endl; // output: 1
cin.ignore(10);
return 0;
}
- 不要混用普通指针和智能指针
ptr1与ptr2虽然是用同一块内存初始化,但是这个共享却并不被两个对象所知道.
这是由于两个对象是独立初始化的,它们互相之间没有通信。当然上面的程序会最终崩溃,因为同一块内存会被析构两次。
std::shared_ptr与std::unique_ptr内部实现机理有区别,前者内部使用两个指针,一个指针用于管理实际的指针,另外一个指针指向一个”控制块“,其中记录了哪些对象共同管理同一个指针。所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的**”控制块“没有互相记录的**。避免使用这种情况,即new和shared_ptr混用。否则析构会出问题。
使用std::make_shared就不会出现上面的问题,他能在分配对象的同时就将shared
_ptr与之绑定。所以要推荐使用。 - 不要使用get初始化另一个智能指针或者为智能指针赋值
shared_ptr<int> p(new int(42));
int *q = p.get();
{
shared_ptr<int> j(q);
std::cout << "s:" << p.use_count() << std::endl; // 1
std::cout <<"k:" <<j.use_count() << std::endl; // 1
} // 程序块结束,q被销毁,内存被释放
std::cout << "o:" << p.use_count() << std::endl; // 0
int foo = *p; // p指向的内存已经被释放, p也称为悬空指针
std::cout << "t:"<< foo << std::endl; // 0 报错free(): double free detected in tcache 2
智能指针陷阱:
(1)不使用相同的内置指针值初始化(或reset)多个智能指针。
(2)不delete get()返回的指针
(3)不使用get()初始化或reset另一个智能指针
(4)如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了
(5)如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
悬空指针,野指针
悬空指针:若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。
将没有用的指针指向nullptr,提供保护
int *p(new int(43));
auto p = q;
delete p; // p,q均无效,都为悬空指针
p = nullptr; // 指出p不需要绑定任何对象
野指针:“野指针”是指不确定其指向的指针,未初始化的指针为“野指针”。
void *p;
// 此时 p 是“野指针”。