【C++】C++ PremierReadNote

C++ Primer 阅读笔记


一、开始


1.输入输出流的数据传输过程

输入:
[窗]->[输入流对象(cin)]

输出:
[输出流对象(cout,cerr,clog)->[窗口]

c++从窗口中读取输入时不是直接存储在变量中而是先存储在istream类型对象cin中,输出是也非直接从变量输出到窗口而是先写入ostream对象cout/cerr/clog中,在从对象传输到窗口。

2.输出运算符“<<”

“<<”运算符输出的是一个ostream类型对象,“<<”接受两个运算对象,其中左侧的运算对象必须是一个ostream对象,右侧可以是需要输出的值,“<<”将需要输出的值写入到ostream对象中。
std::cout<<”num:”<<a<<std::endl;
“<<”连续表达式可以理解为:
((std::cout<<”num:”)<<a)<<std::endl;
每一个括号最终返回的都是一个ostream对象。其中endl是“操纵符”,运行效果是换行,并将与设备关联的缓冲区的内容刷新到设备中去,如果不写endl则在程序运行结束或缓冲区满后系统自动将缓冲区内容刷新到设备中去。
输入运算符“>>”和输出运算符运行逻辑一样。

3.char与unsigned char

在计算机中char也是分无符号和有符号两类的,char与int等类型不同的是,int等类型不显示标注unsigned默认为有符号数,而char在不同硬件环境中表示的类型可能是不一样的,所以为保证移植性最好在使用时指定signed和unsigned,c++标准不建议使用char来存储数值,这种区别主要出现在char类型向值类型的转换的过程中。

4.有符号数与无符号数的运算

在c++运算优先级中,无符号数的优先级大于有符号数,所以当一个表达式中即存在有符号数又不存在无符号数时,运算将被转换为无符号数的运算,由于无符号数表示的最小值是0所以当表达式的运算结果为负数时就会出现运算错误。

5.c++11的四种变量初始化方式

int a=0;
int a={0};
int a{0};

int a(0);

二、变量和基本类型


1.const引用的隐式类型转换
举例使用const限定的int类型的引用来引用double类型的变量的过程:double b=3.14;const int a=b;
在将double类型变量赋值给const int类型的引用时系统先创建一个临时变量,并将double隐式成int类型的结果存放到临时变量中,然后再将临时变量赋给引用,此时引用本质上是int类型的临时变量的别名,之后如果更改了double类型变量的值,引用值随之改变。

2.constexpr变量

constexpr是一个常量表达式,与const不同的是,const限定的变量只能被常量、常量的引用、常量表达式和常量与常量引用的表达式赋值,而constexpr可以被常量、常量的引用、表达式、以及可以在编译阶段就确定返回值的函数赋值。const在程序运行时的本质是在程序用的const的地方直接替换成期对应
的常量,而constexpr则是替换其在编译器期计算出来的结果值。
constexpr的意义在于constexpr强制要求赋值给变量的表达式必须在编译阶段可计算出结果值,这对很多重复运算的简单表达式直接限定了在编译期只进行一次运算起到了优化效果。

3.constexpr指针

被constexpr限定的指针的初始值只能是nullptr或者0或者指向一个地址固定不变的对象,这里需要注意在函数里的任何对象都不是存储在固定地址中的,所以constexpr限定的指针不能指向函数中的对象,全局对象则是存在在固定地址中的所以constexpr限定的指针可以指向这样的对象。

4.c++ 11的两种别名声明方法

typedef bm int;
using bm=int;

5.decltype类型指示符

decltype的作用是通过表达式或函数的返回值动态的推断其指示变量的类型,如:

1
decltype(fun()) sum = x;//sum的类型为fun函数返回值的类型,sum的值由x赋予

顶层Const与底层Const

由于指针本身也是一个对象,所以在Const修饰指针时,就立存在修饰指针或修饰指针所指的对象。如果Const修饰指针则是底层Const,如果修饰指针所指的对象则是顶层Const。

1
2
const int* pa;//const修饰指针,所以为底层const
int* const pb;//const修饰pb,所以为顶层const

三、字符串、向量和数组


1.c++11的string对象的初始化方式

string s;
string str=s;
string str(s);
string str(“str”);
string str=”str”;
string str(2,’s’);//这种形式得到的是字符串”ss”,其中括号里只能使用字符。

2.cin、getline()和cin.getline()

cin在对象在从缓存中读取数据时遇到空格符,制表符,换行符时或读至文件尾时会结束读取。
getline()可以读取空格符,遇到换行符时或读至文件尾时结束读取。
cin.getline()作用和getline一样,只是cin.getline和cin都是标准输出流iosteeam中的对象方法,而getline这是属于string.h中的方法。

3.string::size_type类型

size_type类型是c++定义的几种标准库类型的几种配套类型,其目的在于解决在不同机器中因为机器字长不同而导致string.size()的返回值无法存储的问题,比如在一个16位的机器中int整型存储的最大字符数为32767,但这对于储存一个文件大小的string来说这个大小是明显不够的,所以c++引入size_type,以解决不同机器上string.size()的返回值一定能被存储。
size_type的本质是无符号类型所以在使用string.size()和有符号数做运算时要十分注意,因为这会导致计算结果出现异常。
需要注意的一点是size_type类型实际上不是一个确定的类型,在使用size_type时应注明size_type的类型,如:string::size_type,vector<int>::size_type

4.cctype头文件提供的一些字符判断函数

  • isalnum(c) 当c是字母过数字时为真

  • isalpha(c) 当c是字母时为真

  • iscntrl(c) 当c是控制字符时为真

  • isdigit(c) 当c是数字时为真

  • isgraph(c) 当c不是空格但可以打印时为真

  • islower(c) 当c是小写字母时为真

  • isprint(c) 当c是可打印字符时为真(即c是空格或c具有可视化形式)

  • ispunct(c) 当c是标点符号时为真(即c不是控制字符、数字、字母、可打印空白中的一种)

  • isspace(c) 当c是空格时为真

  • isupper(c) 当c是大写字母时为真

  • isxdigit(c) 当c是16进制数字时为真

  • tolower(c) 如果c是大写字母则输出其小写

  • toupper(c) 如果c是小写字母则输出其大写

5.c++11的“foreach”

c++本没有foreach循环,但是c++11为for语句添加了新的语法:
for(param:array){}这个for语法每次循环从array中读取一个数据并存放在param中用于操作
需要注意的是param如果不使用引用的话,param实际是array中元素的拷贝,此时对param进行修改不会改变array的元素,如需要更改array中的元素的值时需要对param取引用。
还有一点十分重要的是不可以在范围for语句中更改array的容量大小,这是因为能被范围for遍历的序列都需要包含begin,end两个返回迭代器的成员,我们可以先看一下范围for的源码:
for(auto beg=v.begin(),end=v.end();beg!=end;++beg)
{
}
可以看到在范围for中实际上在循环前就已经存储了可遍历序列的begin,end迭代器,所以如果在循环中改变了序列的大小循环就会出错。

6.vector容器的初始化方式

  • vector v v是一个空的vector
  • vector v2(v) v2是一个包含v所有副本的的vector
  • vector v2=v 等价物v2(v)
  • vector v(n,val) v是一个包含了n个重复元素,每个元素都是val的vector
  • vector v(n) v是一个包含了n个重复执行了初始化的对象
  • vector v{a,b,c……} v是一个包含了初始值个数元素的vector,每个元素被赋予对应的初始值
  • vector v={a,b,c……} 等于v{a,b,c……}
  • 使用数组初始化vector容器,vector(begin(array),end(array)),其中begin(array)是数组array的首地址,end(array)是数组array的尾地址

7.vector对象能高效增长

c++标准要求vector容器在运行时能高效快速地添加元素,在c++11标准中vector的动态添加元素的性能要优于预先规定容器大小的性能,只有一种情况例外,就是vector中的元素全部都是相同的时候这种情况会反过来。

8.迭代器的end

迭代器的end()并不实际指向某个元素,所以不能对其进行递增或解引用操作。

9.iterator和const_iterator

iterator和const_iterator是标准库定义的迭代器的类型,一般迭代器没有固定的类型,一般使用类型::iteratorconst_iterator来确定迭代器的类型,如:vector<int>::iterator it
iterator为可读可写迭代器,iterator迭代器只能用于非常量容器;
const_iterator为只读迭代器,const_iterator既可以用于非常量容器又可以用于常量容器,但是常量容器只能使用const_iterator迭代器。

10.cbegin与cend

cbegin和cend与begin和end相对,cbegin和cend返回const_iterator迭代器。

11.迭代器的->运算符

在使用迭代器的过程中经常需要通过解引用后再进行.运算,如:(*it).empty(),操作较为繁琐,c++11对这个操作进行了简化,即通过->运算来替换(*it).,前面的操作就可以替换为it->empty()

12.容器的删添操作会是迭代器、引用和指针失效

这是因为容器的删添操作可能会出现存储空间的重新分配,用string作为例子是最贴切的,当我们想一个string字符串中添加一个字符时,如:

1
2
3
sting v = "str";
v = v + ‘s’;

程序的实际操作是先创建一个临时变量来存储添加之后的字符串strs,然后再将v指向新分配的存储空间,于是指向之前空间位置的迭代器、引用和指针自然就失效了。

13.迭代器的运算

Alt

迭代器之后同时也可以进行加减运算其结果为dfference_type的带符号整型数表示两个迭代器之间的距离。

14.数组的特殊性

不能使数组来初始化数组,也不能使用数组来赋值数组,有的编译器支持数组初始化和赋值这是编译器的自身扩展,但仍然不应该这样做,因为当换了编译环境后程序可能就会出错人。

15.存放存放指针的数组和指向数组的指针

1
2
3
4
5
int *ptr[2];//拥有两个指针成员的数组
int &ptr2[2];//错误,不存在引用数组
int (*ptr3)[2];//指向有两个长度的整型数组的指针
int (&ptr4)[2];//指向两个长度的整型数组的引用

16.数组和指针

数组的名字实际上就是指向数组收地址的指针,所以下面操作得到是指针:

1
2
3
int arr[2] = {};
auto ptr(arr);//per是一个指向arr首地址的int*类型指针

使用decltype进行上面的操作不会得到指针,而是得到数组,如:

1
decltype(arr) arr2 = {1};//arr2是一个包含一个元素的int型数组

对数组执行下标运算实质上是对指向数组首地址的指针执行下标运算,如:

1
int i = ia[2]; //实际得到是(ia+2)所指向的元素

数组的下标运算只要不超过数组长度就可以执行下标运算,如:含有10个元素数组下标从0-9,但是 下标运算可以取到10,即a[10],a[10]是数组的尾元素的下一个位置,但是对a[10]只能进行去地址运算(&a[10]),不能对a[10]解引用或读取。

17.ptrdiff_t类型

size_type一样,ptrdiff_t是c++11专门定制给begin()与end()的运算结果的类型,用于适应不同机器的字长。
需要注意的是,内置数组下标值不是ptrdiff_t类型,如:a[1],1就不是ptrdiff_t类型。

18.void *指针

任何非常量值都可以存入void *指针中。

四、表达式


1.为定义行为

c++中存在一些为定义行为,如<<运算符在c++中并没有定义其后表达式的运算顺序,此时运行下面代码就会出现为定义行为。

1
2
int i=0;
cout<<i<<","<<++i<<endl;

此时会出现0,1或1,1两种结果,这是因为<<没有明确指定何时及如何对运算对象求值,但是在实际使用过程中并没有出现上述情况,这可能是编译器为<<规定了运算顺序。

2.溢出与其他算术异常

6

3.运算符的优先级

Alt

4.前置递增运算符与后置递增运算符的运行过程

前置递增运算符先将对象递增之后再返回递增后的结果,而后置递增运算符则先创建一个临时变量先将旧的值存储起来,然后再将变量递增,再将旧的值返回。
相对于前置递增运算符,后置递增运算符需要创建一个临时变量,这就产生了一个消耗,在一些普通的后置递增运算中可能不大可看得出来,但在较为复杂的迭代器中这种额外的消耗就会产生巨大的性能浪费,所以平时使用时尽量使用前置递增运算符。

5.表达式在运算过程中值发生变化

在一个运算表达式中,参与运算的值不应该在运算的过程中发生变化,否则极易造成未定义行为,如:
v[i++]<v[i],在大于号运算的过程中v[i++]的值向后移动了一位,造成了值的变化,由于C++没有定义二元运算符两端未定义自增运算符的运算顺序,所级就造成了未定义行为。

6.有符号数的位运算

有符号数尽量不要做位运算,因为位运算容易改变有符号数的符号位导致未定义行为。

7.位运算中的类型提升

在程序编写时需要注意,任何占字节小于int类型的类型变量进行位运算时都会产生类型提升而被转换成int类型,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(){
char s='s';
short a=1;
bool b=true;
long c=2;
cout<<sizeof(s)<<endl;
cout << sizeof(~s)<<endl;
cout<<sizeof(s<<1)<<endl;
cout<<"----------"<<endl;
cout<<sizeof(a)<<endl;
cout<<sizeof(~a)<<endl;
cout<<"----------"<<endl;
cout<<sizeof(b)<<endl;
cout<<sizeof(~b)<<endl;
cout<<"----------"<<endl;
cout<<sizeof(c)<<endl;
cout<<sizeof(~c)<<endl;
cout<<"----------"<<endl;
cout<<~s<<endl;
cout<<~b<<endl;
return 0;
}

输出结果:
Alt

8.sizeof计算指针大小

sizeof在计算指针时有个很有趣的现象,因为sizeof满足右结合律且优先级与*相同,所以在sizeof *p中,先对p解引用,然后在对解引用后的内容计算所占内存大小,由于sizeof并没有使用解引用的内容,所以*p是否为空是否有效对sizeof的计算没有影响。
sizeof *p == sizeof(*p)

9.隐式转换的过程

int a=3.14+3;为例
在这个例子中3.14是山比double类型,3是int类型,运算时为了保证精度C++会将int类型转换成double类型,而不是因为结果是int类型而将double类型转换成int类型,所以在表达示右侧的运算过程得到的结果是一个double类型的值,当这个值被赋值给a的时候才进行double到int的类型转换。


五、语句


1.switch语句跨case标签引用变量

若需要在一个 case 语句下定义变量同时在多个 case 语下使用这个变量,则定义变量时不能同时初始化变量,应另起等于初始变量,否则编译器会报跨标签访问错误,如:

1
2
3
4
5
6
7
8
9
10
11
12
switch (a)
{
case 0:
int b; //如果直接int b=1;则会报跨标签访问错误
b=1;
break;
case 1:
cout<<b<<endl;
break;
default:
;
}

2.C++无法捕获计算溢出异常和除0异常

C++认为计算溢出和除0异常属于底层事件,应被同样底层的事件去处理,所以C++没有提供计算溢出和除0异常,如果需要捕获这两类异常在Windows系统中应使用Windows提供的SEH模型__try__except__finally异常语句,使用方法和try-catch基本类似,与try-catch不同的是,SEH中使用的是__try-__except搭配和__try-__finally搭配。


六、函数


1.静态局布变量

在函数体内使用static关键字可以效静态局布量,静态局布变量的生命周期从变量定义到程序结束,但是静态局布变量只对函数可见,在函数外不可访问。

2.C++程序的编译过程

Alt

3.函数调用过程中的形参创建

函数的每一次调用都会重新创建形参并传入实参对其进行初始化,形餐盘类型决定了形参与实参的交互类型,如果形参是引用类型,则形参与相对应的实参相绑定,作为实参的别名使用;如果形参是值类型,则实参的值拷贝给形参,形参与实参相互独立。

4.函数的指针参数传递

当指针作为实参传递给函数形参时,传递的是指针的值拷贝而不是指针所指的对象,且形参指针与实参指针是两个不同的指针,此时形参指针与实参指针指向同一对象,修改形参的指针指向不会影实参指针。

5.引用形参的限制

  • 如果将函数的参数定义普通引用参数,那么这个形参就无法接受const对象、字面值和需要类型转换的对象作为实参。

  • 如果将函数的参数定义为const引用参数,这个参数可以接受第一条中的参数,但是却无法修改参数的值。

6.数组的两个特殊性质

  • 数组不可被拷贝
  • 数组再使用时被转换为指针

7.数组引用形参

当我们使用数组作普通形参时,

1
2
3
void fun(int* arr);
void fun(int arr[]);
void fun(int arr[10]);

表示的都是同一个函数,且形参可接受任意长度的实参数组,但是当将普通形参改为引用形参时,

1
void fun(int (&arr)[10]);

(&arr)中()不可缺少,否则形参变成引用的数组而非数组的引用,且形参只能接收长度为10的数组。

8.main函数传参

有的时候我们会遇到这样的main函数:

1
2
3
int main(int argc,char** argv)
{
}

通常情况下,直接使用argv,argv中的内容是空的,那么main函数在什么情况下会接收参数呢?
事实上main函数本身就是当前程序的入口,在当前程序中没有其他程序为main函数传入参数了,当我们把写好的程序打包后在外部调用时就可以给main函数传入参数了,如在CMD中调程序

1
prog -o -d ofile data0//prog为程序名

需要注意的是argv数组的0号索引存储程序的名字,参数从1号索引开始存储,且最后一个索引位置固定内容为0


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!