C++ Primer 第一章 快速入门
1.1 编写简单的C++程序
任何一个都要包含一个main函数,因为这个函数是系统执行入口c++也一样,函数格式是 int main(); 和别的主要函数不同C++的主函数有且只有一个且返回一个INT类型的值。一般来说返回0表示执行成功。程序员可以不必定义return 0返回这时候系统会在编译时自动加上retuan 0;
1.2 偷窥输入输出
这里指的输入输出是说从标准设备输入内容或者将内容输出到表顺设备。比如说输出字符到显示器,输出一个图片到显示器。将字符或者二进制内容输出到文件。 或者从键盘输入内容到计算机,从一个文件读取内容到系统等等。
C++提供了标准输入输出库,最常用得是iostream 它的父类 中有istream / ostream 因此可以看出它是输入输出的的功能组合类。一般来说我们常用到的有这么几个
cint; cout; endl; 下面是他们的一个应用实例:
{
std::cout << " 请输入 " << std::endl;
int v1,v2;
std::cint >> v1 >> v2 >> std::endl;
std::cout << " 用户输入了: " << v1 << " and " << v2 << std::endl;
}
这里std::表示是在std这个命名空间下
std::count << “string” 表示要输出字符串 “string”
std::cint >> v1 表示系统等待用户输入一个内容且输入内容会保存到变量v1中
std::endl 是一个操纵符 表示立刻刷新缓冲区,这样输入输出会立刻生效输出的内容会立刻被呈现在屏幕或文件上。
由于这些功能属于标准库儿不是内置类型,所以在使用前应该导入对应的库 #include<iostream>
1.3 注释
和大部分语言一样,类或者代码块的说明以/*开头,以*/结束,可跨行,单行注释用//
1.4 控制结构
有 if , for , while 三种用法,使用和其他语言类似。
可以通过while和std::cint的结合读取多个内容,代码如下:
{
int v1;
while(std::cint >> v1)
{
std::cout << " 用户输入了: " << v1 << std::endl;
}
}
while(std::cint >> v1) 语句会持续让用户输入。std::cint >> v1 将用户输入内容并保存到变量然后返回std::cint对象。
用户输入以回车符为输入结束,语句判断用户输入的内容是否有效若有效返回一个有效的std::cint对象,while判断为true则执行循环体内语句,若输入非法则退出循环继续向后执行。
1.5 类的简介
C++中类型分为内置类型和类类型,类属于类类型。类是一种复杂的数据结构支持继承,C++中的类结构上和其他高级语言类似,但由于指针,引用,常量的大量组合使用使得它类的复杂度比一般语言要高的多。类有非常多得灵活应用,意味着有很多需要掌握的内容,只有深刻理解这些基本概念才能用好学好类用好类因为类本质上是对这些基础概念的一个有序组合。
C++ Primer 第二章 变量和基本类型
2.1 基本内置类型
基本内置类型是C++“自带”的类型,区别于标准库定义的类型。使用时不需要应用标准库就可以使用,我们可以理解为数字型有下面这些
整形:就是整数或者是没有小数位的数。它包括bool(0,!0) , char, wchar_t(非标准英文字符用char无法全部表现所以需要这个类型来表示),short, int ,long。 整形中除了bool外其他类型可以是带符号,也可以是无符号的,无符号的取值范围不能为负,有符号取值可以有正有负,但由于有符号数所占位其中一位是符号位所以它的正数取值范围要比无符号范围小一位。
浮点型:就是带小数的数,包括float , double , long double他们之间的区别是取值范围和精度,可以根据你的需要选择合适的类型。
char , wchar_t 两个类型虽然都是字符型但区别比较大:
在宽度上来说,一个是1byte,一个是2byte(在linux上实际是4byte)
在编码上来说 wchar_t表示unicode编码方式
以上不同说明 wchar_t 可以包含更多内容比如中文,日文等等。
2.2 字面值常量
就是常量本身,比如 21, “name” 表示类两种类型的字面值。有些类型有多重表示法,比如数字可以用不同进制表示,浮点型可以用科学计数法书写等等。
2.3 变量
变量是对数据的有名存储,既然有名就可以对改数据做一些操作如赋值修改等等。这里有两个表达式概念先了解一下
左值:左值可以出现在赋值语句的左面或者右面
右值:右值只能出现在赋值语句的右面儿不能出现在左面
变量是左值,常量是右值,字面值常量也是右值
变量定义和初始化值更具类型不用也不同,内置类型不能隐身初始化,类类型一定要可以隐式初始化。
先看内置类型:
int a ; // 没有初始化内容,它的值由系统分配规则是在栈区和堆区(函数内定义或者类里面定义)都取随机值,在全局区(全局常量,静态变量)都是全零值。
int b=1 ; // 这种叫赋值初始化
int c(1),d(b + 2) ; // 这种叫直接初始化()中不一定是常量可以是一个表达式或变量
这两种初始化是不一样,后面讲到类类型时就会明白一般来说直接初始化更灵活且效率更高。
需要特别强调的是未初始化的内置类型虽然也会有值但是其分配规则导致其值不确定性,所以我们定义内置类型时一定要初始化值而不依赖系统的内部非配规则。
再看类类型定义:
string a; // 隐式初始化初始化成了 “”
string b = "name" ;
string c(b) ;
string d(10,'a') ; // string类特有的初始化方法,等同于10个字符a组成字符串并赋值初始化给变量b
类类型的初始化其实就是构造函数, 隐式初始化实际上就调用类型的默认构造函数来初始化类型对象;直接初始化是调用类的拷贝构造函数;而直赋值初始化是调用赋值操作符重载和拷贝构造函数。相关内容再后续章节会有详细说明。
声明和定义
c++是一门奇怪的语言,我们先看如下代码
{
std::cout << v1 << std::endl;
}
int v1 = 1;
绝大部分语言中这样的写法是没有问题的,但在C++中却会编译不通过,因为C++是严格的顺序编译非类对象代码的,当main函数编译时要用到的变量v1还没有执行定义,c++就会抛出异常说使用了未定义的变量v1,如果吧这个 int v1;语句放到函数前面就没问题。那么我们是不是只能这样做呢? 答案当然是否定的,我们可以不改变现在代码顺序而使用声明解决这个编译错误,可以这样修改代码:
{
extern int v1;
std::cout << v1 << std::endl;
}
int v1;
注意红色新增代码就是一个声明。他的意思是说:HI,系统中的某个地方定义了一个叫v1的int变量,所以你可以放心的使用。
声明是告诉编译器一些信息所以不会分配内存空间也不会产生具体的数据。 声明的对象一旦被使用就一定要在某个地方定义它,否则编译器即使暂时不抛出异常在编译完毕后发现根本没有地方找到相关定义也一样会编译失败。
以上例子只适合变量,同一个文件中常量在使用前一定要先定义,否则即使做了申明也会出错。
声明最大的好处体现在多文件的编程中,比如说我编写了一个头文件并引入到了主文件中,头文件需要使用主文件定义的变量。由于头文件一般是先于主文件编译执行的,所以就会出现未定义的编译错误,如果在头文件中先声明一下这变量再使用就没有问题了。 除了变量C++还有常量(后面会讲到),它和变量有一些不同
extern int a;
// usend a
extern const int b; // 常量声明语法也可写成 const extern int b
头文件中用到了两个对象,但他们是在其他地方定义的所以我们需要申明一下在使用。
#include head.h
int a(1); // 这样定义的变量可以被其他相关文件(head.h)声明并使用
// const int b(2); 对于常量这样写不行是因为这样定义的变量作用范围只在本文件中,头文件虽然做了声明但无法使用这个常量,应该用下面的语法来定义
extern const int b(2); // 这样定义的常量才可以被其他相关文件(head.h)声明并使用
int main()
{
// do...
}
主文件中定义了头文件需要的对象,和变量不同,常量如果在其他地方被申明和使用一定要在定义的时候加上关键字extern因为如果不加的话常量默认作用范围是定义它的那个文件,变量默认是所有相关文件。
虽然C++支持面向对象,但他并不是完全的面向对象他也支持面向过程。因此有一些代码是不属于任何类的,比如说main 函数就不属于任何类。
针对这种情况变量根据其位置可分为两种 全局变量局部变量。广义上说定义在类内部(包括类所属函数内部)和函数内部的变量叫局部变量,除此之外的变量自然就是全局变量了。
2.4 const限定符
限定符表示定义一个常量,常量一但定义则不可更改。不可更改的意思是既不能对常量句柄句柄重新赋值也不能更改常量对象的数据成员(如果是一个自定义类常量C#中是可以更改类成员的但C++不允许这么做)。
常量分为两类 编译时常量 和 运行时常量(在.NET中标示为readonly) 编译时常量是定义后直接初始化的常量,运行时常量值要初始化的值必须要通过代码运行才可以确定的
const int b = getval(); // 运行时常量值来自一个函数的运行结果
const myclass my(1,"tom"); // 自定义类的常量定义都是运行时常量,因为需要运行类的构造函数
常量初始化之后就不可以做任何的更改操作。
2.5 引用
引用就是对某个对象的另外一个别名。引用最重要的作用是函数传参。
int val = 1;
int &refval = val; // 引用了一个变量
int &refval1 = 12; // 错误,12是个字面常量,常量必须要用常量引用
refval = 2; // 等价于 val = 2
常量引用
const int val = 1;
const int &ref1 = val; // 引用了一个一般常量
const int &ref2 = 12; // 引用了一个字面常量
int &ref3 = val; // 错误,常量必须要使用常量引用,ref3是个变量引用
int var = 2;
const int &ref2 = var // 常量引用指向了一个变量,这时候
refval2 = 3; // 不允许通过常量引用来做任何更改操作
varval = 3; // 但是可以用原始变量来更改内容
总之常量引用可以引用常量或变量,但是无论如何都不能通过引用来更改数据内容。
变量引用只能引用变量,用引用可以更改内容效果与用原始变量更改内容是一样的。
对于引用还有一点很重要:非常量引用类型必须严格匹配,常量引用可以在内置类型之间相互引用
int &b = a; // 错误,类型不匹配
const int &c = a; // ok
// 这个操作实际等同于
int temp = a;
const int &c = temp;
如果对非const引用b不做类型匹配限制,b实际就会引用临时变量temp,对b的修改无法反应到变量a,引用失去了其意义。 const引用c没有这样的问题,因为它本身不允许修改。
2.6 typedef
这个关键字非常有用,用来给某个类型指定一个别名,比如
typedef int zx ;
int a(1) ; // 等同于 zx a(1)
在后面我们可以看到使用一些复杂类型或是函数指针时改关键字可以大大减少你的代码整洁度。
2.7 枚举
枚举是一组可选常量值,既然是一组可选值说明包含多个常量。枚举定义语法如下
enum val{val1 = 2, val2 = 4, val3} // 最后一个内容没有显示给值等价于 val3 = 5
如果不指定值默认第一个值从0开始下一个依次+1递增。
枚举的每一项都是一个唯一的const类型值,上面的定义有点类似于:
const val1 = 2; const val2 = 4; const val3 = 5;
由于是const的,所以 val2 = 1 或者 val a = 2; 都不允许。
枚举项和int类型值有对应关系,但是二者只能单向转换,枚举可以自动转成int,而int却不能转成枚举
val a = val2 ; // 枚举之间赋值初始化
int b = val2 ; // 枚举转成int并初始化
val a = 2 ; // int 不能转成枚举,无法初始化
2.8 类类型
一般来说C++吧除了内置类型之外的类型都叫类类型,我们习惯上把自定义的类class称为类类型。 类一般采用先定义类并声明类的成员函数,然后在外部定义成员函数的语法形式。
这部分内容不在这里详述会在后续章节中专门说明。
2.9 编写自己的头文件
从上面的课程我们应该大概知道什么是头文件,一般来说头文件中包含声明而不是定义,但是下面两种情况比较特殊
类定义要放在头文件里。这样如果某个文件需要这个类只需要把头文件include进来即可。
运行时常量也可以在头文件定义,表达式可以在包含文件中定义。这样就能实现在在不同的包含文件得到不同的常量值。
int getval();
const int p = getval();
// mast1.cc
#include " head.h "
int getval()
{
return 100;
}
int main()
{
cout << p << endl; // 输出100
}
// mast2.cc
#include " head.h "
int getval()
{
return 200;
}
int main()
{
cout << p << endl; // 输出200
}
有时候某个头文件可能会被包含多次(直接包含+间接包含)。比如文件包含了头文件A,也包含了头文件B,头文件B同时也包含了A,则文件重复包含了A。这个时候如果A中有定义语句就会产生“重复定义”的编译错误,这个时候可以用#ifndef 把头文件的内容都放在#ifndef和#endif中吧。不管你的头文件会不会被多个文件引用,你都要加上这个。一般格式是这样的:
#ifndef <标识>
#define <标识>
...... 头文件代码
#endif
<标识>在理论上来说可以是自由命名的,但每个头文件的这个“标识”都应该是唯一的。标识的命名规则一般是头文件名全大写,前后加下划线,并把文件名中的“.”也变成下划线,如:stdio.h
#ifndef _STDIO_H_
#define _STDIO_H_
...... 头文件代码
#endif
该预定义标示不单能防止头文件被重复包含编译,而且还可以用在不同头文件定义同名对象时出现异常的处理上。
C++ Primer 第三章 标准库类型
3.1 命名空间的using声明
using声明是对某个命名空间做引入。主要作用是简化代码编写。
3.2 标准string类型
首先要明确类型是类类型,意味着它有构造函数,也类似我们自定义的类一样的其他类对象。
它有几种初始化方式如下
string s1 ; // 调用默认构造函数初始化对象
strng s2(s1) ; // 将S2初始化为S1的一个副本
string s3("value") ; // 用一个字符串值初始化对象
string s4(n,'c') ; // 用N个字符‘C’组成字符串作为初始化s4的值
特别要注意的是第一种初始化方式,虽然默认构造函数是没有参数的但是不能因此就写成 string s1()
我们可以复习一下内置类型的默认初始化方式和类类型做个比较:
string s1 ; // 调用默认构造函数初始化
int i ; // 要根据定义位置来确定初始化值,全局变量一律初始化为0, 局部变量是一个随机数,称为未初始化
string类型可以用于标准输入输出。
while(getline(cin, line))
{
cout << line << endl;
}
cin >> line 输入内容并保存到变量line,输入时会忽略输入左面的的空格直到非空格字符才开始读取,直到再次读到空格输入结束。
例如如果你输入" zhang san " 实际line保存的是"zhang" 。
getline(cin,line) 是一个系统函数,可以输入标准行内容,这个函数不会忽略任何内容一直读取用户输入并保存到line,直到用户输入换行函数才结束,结束时函数会返回cin的引用。 如果用户刚开始就输入换行符那么line的内容就是"" 。
上文说过string是类类型所有有很多类成员(属性和成员函数),下面就是一些常用的操作
s.empty() ; // 判断s是否为空,相当于s.size()==0
s.size() ; // s的长度
s[n] ; // n位置的字符(左值返回)
s1+s2 ; // 返回s1和s2连接的串
s1=s2 ; // 把s1替换为s2的副本
s1==s2 ; // 判断s1,s2是否相等
!=,<,<=,>,>= // 按字典顺序比较
s.insert(...) ; // 插入字符操作,有多个重载可用
s.size()函数返回一个表示字符串长度大小的值,其类型并不是我们认为的int类型,而是一个叫string::size_type的类型,为什么不用INT而新创造一个类型呢,原因有如下两点:
1. 取值范围不同,int有固定的取值范围,并且可以取负数,但字符串长度是不可能为负的,并且长度的大小很可能会超过int的范围而导致溢出
2. int的范围大小与机器相关,有的机器上范围大些,有点机器小一些。但是字符串长度应该是个不能随机器发生大小改变的值,所以即使用无符号int来表示串大小也是不合适的
s[n]可以作为左值操作,也就是说改操作既可以返回N位置的字符也可以替换N位置的字符。 n值一定要在有效范围内,负值或者超过串大小会引发严重异常
a[1] = 'b' ; // 字符一定要用单引号,双引号表示字符串
cout << a << endl; // 输出 "abaa"
cctype 头文件所包含的函数主要用来测试字符值,以下是一个列表,但是对于初学者来说自己上机操作一下,后两个返回的是int型,确实很意外,强制转换一下,很简单。
isalnum(c) ; // 假如c是字母或数字,则为true
isalpah(c) ; // 假如c是字母,则为true
iscntrl(c) ; // 假如c是控制字符,则为true
isdigit(c) ; // 假如c是数字,则为true
isgraph(c) ; // 假如c不是空格,则为true
islower(c) ; // 假如c是小写字母,则为true
isprint(c) ; // 假如c是可打印的字符,则为true
ispunct(c) ; // 假如c是标点符号,则为true
isspace(c) ; // 假如c是空白字符,则为true
isupper(c) ; // 假如c是大写字母,则为true
isxdigit(c) ; // 假如c是十六进制数,则为true
tolower(c) ; // 假如c是大写字母,则返回小写字母形式(对应的int值),否则返回c。
toupper(c) ;// 假如c是小写字母,则返回大些字母形式(对应的int值),,否则返回c。
可以举个简单的例子
isalpah(line[0]); // true
isdigit(line[1]); // true
ispunct(line[4]); // true
tolower(line[0]); // 返回A对应的ascii 65
3.3 标准vector类型
C++标准库容器有好几类,后面会详细介绍。为什么在这里单单要先介绍vector容器呢?这个容器最常用。对于大部分应用来说用它足以满足你的要求。
vector是个类模板,如果你了解JAVA或C#范型编程的话可以理解为范型类。泛型最大的好处是只需定义一个类或函数就可以提供不同类型版本的操作。
它的初始化有如下几种方式:
Vector<T> v1 ; // 默认构造函数v1为空
Vector<T> v2(v1) ; // v2是v1的一个副本
Vector<T> v3(n, i) ; // v3包含n个值为i的元素 参数 T 如果是类类型则一定要有拷贝构造函数(未定义的情况下系统会自动分配一个)
Vector<T> v4(n) ; // v4含值初始化的元素个副本 参数 T 如果是类类型则一定要有默认构造函数(未定义的情况下系统会自动分配一个) 如果是内置类型则分配n个0
对于类类型如果不能满足红色标示的要求编译会失败。 关于类类型的拷贝构造函数和默认构造函数后续章节有介绍
Vector对象有几种最重要的操作
v.push_back(t) ; // 在数组的最后添加一个值为t的数据
v.size() ; // 当前使用数据的大小 返回vector<T>::size_type类型的长度值,其意义类似上面讲过的string::size_type
v.empty() ; // 判断vector是否为空
v[n] ; // 返回v中位置为n的元素 和string类型下标操作类似 是个左值操作
v1=v2 ; // 把v1的元素替换为v2元素的副本
v1==v2 ; // 判断v1与v2是否相等
!=、<、<=、>、>= ; // 保持这些操作符惯有含义
关于vector需要注意的是下标不能用来添加元素操作。
list[ 0] = 1; // 错误 list没有任何数据,下标操作只针对存在的元素
list.push_back( 2) ; // 增加了一个值为2的元素
list[ 0] = 1 ; // ok 将第一个元素由2更改成1
3.4 迭代器简介
迭代器是用于对容器做遍历操作的类型。 迭代器和后面要说到的指针非常类似。但它侧重列表的迭代遍历,所有有一些快捷属性可用。
list.begin() // 表示容器的第一项迭代器,如果容器有值指向list[0]
list.end() // 表示容器的哨兵位,也就是最后一项后面的一项,只是用来表示迭代器已遍历到容器末端
iter++ // 迭代器自增表示向后移一位,指向下一个项
vector<string> list(10,"value");
for(vector<string>::iterator iter = list.begin(); iter != list.end(); iter++)
{
cout << *iter << endl; // 要访问迭代器当前值必须解引 *
*iter = "new value"; // 也可以解引后更改其值,可以看出解引是左值操作
}
// 如果想遍历容器又不想使用迭代器可以用下标操作
for(vector<string>::size_type ix = 0; ix != list.size(); ix)
{
cout << list[ix] << endl;
list[ix] = "new value";
}
当容器为空时 list.begin() == list.end()
上面定义的是常规迭代器,还有一种叫常量迭代器的迭代器类型。 我们也能定义迭代器常量,要弄清楚这些拗口的概念可以看下面代码示例
*iter = "new value"; // 这是不允许的,因为const_iterator告诉编译器我代表的是一个常量,所以不能通过任何手段改变其值
const vector<string>::iterator iter // 定义一个常迭代器,迭代器代表的变量,但迭代器本身是常量,所以可以更改代表的内容但无法更改迭代器
*iter = "new value"; // 没问题
iter ++ // 不允许,迭代器是常量所以无法让他指向其他项
const vector<string>::const_iterator iter // 这样定义的迭代器只能读取初始化指向的列表项内容,既无法向后移动迭代器也无法更改项值内容
最后一个定义很有意思:迭代器指向了常量,所以不能通过解引更改常量值,同时迭代器本省也是常量所以无法更改迭代器的指向
迭代器不是每次只能向后移动一位,可以通过迭代器与一个整形字面值相加向后移动多位
iter + n // 迭代器向后移动n位并产生一个指向移位后新位置的迭代器
两个迭代器可以做相减运算结果是类型为difference_type的两个迭代器之间的距离(两个操作数一定要指向同一容器否则报错)
3.5 标准bitset类型
标准库中bitset类型用来处理二进制位的有序集,bitset类型简化了位集的处理,使用bitset时需要包含头文件#include<bitset>
bitset对象的定义和初始化
bitset也是类模板,不过bitset类型对象之间的区别在于长度而不是类型,因此bitset模板的参数是长度类型
初始化方法 | 说明 |
bitset<n> b; | b有n位,每位都为0 |
bitset<n> b(u); | b是unsigned long型u的一个副本 |
bitset<n> b(s); | b是string对象s中含有的位串的副本,s是01串 |
bitset<n> b(s, pos, n); | b是s中从位置pos开始的n个位的副本 |
用unsigned long值初始化bitset对象
用unsigned long值初始化bitset对象的时候,将long值转化为二进制模式,然后拷贝到bitset的右方(bitset右边为低阶位,左边为高阶位),string位数多了将被截断,少了bitset将在前面补零。
用string对象初始化bitset对象
从string对象初始化bitset对象,需要注意的是,复制拷贝相当于从string位模式平移到了bitset。
例如:
string str(”11001010”);
bitset<32> bitvec(str);
这个时候,bitvec是这样的:0000 0000 0000 0000 0000 0000 1100 1010最右边是bitset的低阶位,即bitvec[0],bitset[1] …
bitset对象上的操作
操作调用方式 | 操作说明 |
b.any() | 测试b中是否有存在1的位 |
b.none() | 测试b中是否全0 |
b.count() | 测试b中置1的位个数 |
b.size() | b中所有二进制位个数 |
b[pos] | 访问下标为pos位置的位值 |
b.test(pos) | 测试pos位置的二进制位是否为1 |
b.set() | 将b所有位置1 |
b.set(pos) | 将b中pos位置的位置1 |
b.reset() | 将b所有位置置0 |
b.reset(pos) | 将b中pos位置的位置0 |
b.flip() | 将b中所有位翻转 |
b.flip(pos) | 将b中pos位置上的位翻转 |
b.to_ulong() | 将b转化为unsigned long值 |
os << b | 将b的位集合直接输出到os流 |
注:
Ø b.cout和b.size()返回的是size_t类型,该类型定义在cstddef头文件中(C标准头文件stddef.h的C++版本)
Ø b[pos]可以作为左值,即可以用来改变pos位置的值
C++ Primer 第四章 数组与指针
4.1 数组
数组是同一类数据的集合。数组的特点是 以顺序结构结构存储,一点定义就无法更改数组大小。
数组定义很简单:
int a[2] ; // 定义了一个能容纳两个int类型数据的数组
const int sz = 2 ;
myclass ls[sz] ;
定义数组的时候系统可能会自动初始化数组的每个项目,但也可以显示提供值。
int a[3] {1,2,3} ;
可以既指定数组大小又提供初始化列表,此时列表内元素数不能大于维数; 如果列表提供元素小于维数则从数组第一个元素开始显示初始化,没有提供的数组元素会自动初始化步。
自动初始化遵从下面规则:
Ø对内置类型来说:
int a[2] ; // 表示要自动初始化。如果a是全局定义的则每一项都初始化为0,否则会分配随机数,表示未初始化
也可以显示给值例如:
int a[] {1,2,3} ; // 显示初始化时候可以不必提供数组大小,系统会自动推断大小
Ø对于类类型来说:
myclass ls[2] ; // 无论在是全局定义还非全部定义都会调用类型的默认构造函数,如果类没有默认构造函数则编译出错
Øchar类型数组比较特殊:
char a[]{'a', 'b', 'c'} ; // 标准定义
char b[]{'a', 'b', 'c', '\0'} ; // 一个C风格的字符串,等价于 char b[] = "abc" 二者的字符数组大小都是4
上面两种定义方式唯一不同的就是最后一个字符是否是 '\0' (结束符)如果是则表明定义了一个C风格的字符串。
要注意:虽然char b[] = "abc" 内容只有三个字符,由于这种定义是C风格字符串的定义法所以系统会自动在对应的字符数组后面加上 '\0'
在指定大小定义C风格字符串时要注意前后长度匹配 char b[3] = "abc" 会产生错误,需要存放四个字符大小。
和其他类型定义不同,数组定义是没有类似 int a[](b) 这种定义方式的,因为数组不支持复制操作。
数组最常用的操作办法是小标操作 a[0] 表示第一个项,它是左值操作。
表示数组下标和数组大小的数据类型和bitset一样是size_t 但是数组只有下标操作,没有取大小等其他操作。
int ls[sz];
for(size_t i = 0; i < sz; if++)
{
cout << ls[i] << endl;
ls[i] = i + 1;
}
4.2 指针的引进
指针一般是对对象的存放地址的直接指向,一般定义形式为:
int *cur ; // 定义了一个未初始针未初始化指针值是个随机数
int *cur = 0 ; // 可以给指针赋予初始0值表示该指针没有指向任何对象,不应对其有任何操作 。这个赋值表达式中0值是有约束的,必须是字面常量或者编译时常量值0
int *cur = 121 ; // 也可以给指针赋予一个整数常量,但最好不要这么做。因为该常量表示内存地址而我们并不十分清楚这个地址中存放了什么数据。一单指针做了某些操作就可能破坏这些数据,对一个未知的内存做指针指向是危险的(未初始化指针情况也类似)
应该在定义是就给予其初始化工作:
int a = 123 ;
int *cur = &a ;
有一类比较灵活的指针 *void 可以指向任何类型的变量;
int a = 123 ;
string b = "abc" ;
void p1 = &a ;
void p2 = &b ;
这类指针表示指向某个内存地址的数据,但不清楚改数据类型。void指针操作有限。由于不知道指向的数据类型所以不能对指向数据做任何操作。
有一类指针可以指向另外的指针,也就是指向指针的指针定义如下:
int a = 123 ;
int *cur = &a ;
int **curr = &cur ; 这类指针需要做两次解引(**curr)才能获取真正的对象值。
指针操作一般有两个意思:
Ø对指针指向的数据做操作(需要解引):
int a = 123 ;
int *cur = &a ;
*cur = 456 // 等价于 a = 456; (对所指向的数据做操作需要做解引在做操作(就是在变量引用时保持*号))
Ø对指针本身操作是指改变指针的指向对象:
int a = 123 ;
int *cur = &a ;
int b = 123 ;
int *curb = &b ;
a = b ; // 等价于 a = &b (对指针本身操作不能带解引符号*) a, b两个指针现在都指向了变量 b ,变量 a 现在没有任何指针指向。
上面讲了数组的基本概念,数组和指针有着非常紧密的联系。 由于数组本身只有下标操作太过简单,所以大部分情况下都使用指针来操作数组。
指针不能直接指向数组变量,它是通过指向数组中得某个项并前后移动来操作数组;
int a[count] = {1,2,3,...} ;
int *cur = a ; // 该操作是简化操作实则是指针指向了数组的第一项,等同于 int *cur = &a[0]
也可以明确指定 int *cur = &a[2] ;
指定了数组的指针可以前后移动,导航到当前项的前后项指针:
int *next = cur +1 ;
int *prv = cur -1 ;
int *next3 = cur +3 ; // 当前项向后移动三位的项指针
数组在数据范围前后一位溢出位的指针表示叫哨兵位,一旦指针到了任意一个哨兵位就不能再做一般化处理。
int a[10] = {1,2,3,...} ;
int *cur = &a[0] ;
int *cur1 = cur -1 ; // cur1是哨兵位,表示指针已经移出数组
int *cur2 = cur +10 ; // cur1是哨兵位,表示指针已经移出数组
哨兵位的作用是标识指针已经超出了数组有效范围
两个数组指针可以相减,值是类型为 ptrdiff_t 的项间距(两个操作数中不能有哨兵位)如下:
int a[] = {1,2,3,...} ;
int *cur1 = &a[1] ;
int *cur2 = &a[5] ;
ptrdiff_t pd = cur2 - cur1 ; (也可以交换两个操作数的位置,结果允许为负数)
指针可以在数组有效范围内任意移动,找到相应项后如何操作数组的真实值呢? 只需要解引既可。
int a[] = {1,2,3,...} ;
int *cur = &a[1] ;
*cur = 1 ; // 等同于 a[1] = 1
数组最重要的是下标操作。数组指针也有下标操作,但是意义和数组下标不一样
int a[] = {1,2,3,...} ;
int *cur = &a[1] ;
cur[1] = 2 ; // 等价于 *(cur + 2) = 2 也等价于 a[3] = 2
int i = cur[-2] ; // 等价于 int i = *(cur - 2) 也等价于 int i = a[1]
指针是数组的迭代器,最后让让我们看看用指针如何实现迭代
int arr[count] = { 1, 2, 3...};
for( int *cot = arr; *end = arr + count; cot != end; ++ cot)
{
cout << *cot << endl;
}
可以用const修饰定义指针。
Ø指向常量的指针:
const int a = 123 ;
const int *cur =&a ; // const 必须
需要注意:常量的指针必须是指向常量指针,但指向常量指针可以指向常量也可以指向变量。无论如何都不能对它所指内容做更改,即使它实际指向了变量,但可以更改这个指针的指向
int c =456 ;
cur = &c ; // 更改了指针的指向,现在指向的实际是个变量
*cur = 789 ; // 不允许,虽然指向的是变量但系统认为是常量所以不允许修改
Ø常量指针:
表示不能重新再做指向更改,但也许可以修改它指向的对象的值,这取决于它指向的值是变量还是常量 如下:
const int a = 123 ;
int b = 456 ;
int *const cur1 = &a ; // 此时不允许允许 *cur = 789,因为指向了一个常量
int *const cur2 = &b ; // 此时允许允许 *cur = 789,因为指向了一个变量
cur1 = &b ; // 错误,不允许更改指针的指向
需要注意的是这类指针在定义时必须初始化。
如果我定义了这样一个指针
const int *const cur = &a;
那么既不能更改指向的数据值,也不能重新指向其他数据
4.3 c风格字符串
前面内容了解了char 数组可以表示c风格字符串,既然数组可以用指针表示那么char*也可以表示c风格串
char a[]{'a','b','c','\0'} ; // 第一种数组定义语法
char a[] = “abc” ; // 第2种数组定义语法
char *a = “abc” ; // 指针表示法
c风格的字符串有多个操作函数: strlen(), strcpy(),strcat()以及strcmp()
函数中所说的结束标示NULL实际上就是最后一个字符 '\0' 。
char s2[20] = {'A','N','S','I','\0','C','+','+'};
char s3[6] = {'I','S','O','C','+','+'};
cout<<strlen(s1)<<endl; // 5
cout<<strlen(s2)<<endl; // 4
cout<<strlen(s3)<<endl; // 不确定
strlen(s1)==5,原因是{'m','o','b','i','l'};指定一部分内容的时候,剩余部分会自动赋值为空字符,而'\0'是转义字符表示的就是空字符.
strlen(s2)==4,因为第五个字符是字符串结束符'\0'(==0)。
strlen(s3)==?,因为他没有结束符。
可以创建动态数组,动态数组创建时更具类型不同其初始化值也不同。
int *a = new int[10] ; // 包含10个未初始化元素的数组
int *b = new int[10]() ; // 包含10个初始化为0的元素数组
// 类类型
string *c = new string[10] ; // 包含10个调用了类默认构造函数完成初始化的元素数组
可以看到内置类型需要在后面加上()才可以让元素初始化。
动态数组定义之后一定要手动删除掉,否则会造成内存泄露
delete [] a ; // 方括号必不可少,表示要释放一个动态数组
C++ Primer 第五章 表达式
本章内容比较简单,只做部分说明
1.箭头操作:
c++ 中箭头操作是个复合操作,将解引和调用组合调用了
myclass clsobj ;
myclass *cls = &clsobj ;
cls->show() ; // 等价于(*cls).show()
2. sizeof操作
返回一个对象或类型名的长度,这个操作比较重要,可以查看这篇文章
3. new和delete
new和delete运算符用于动态分配和撤销内存的运算符
Ønew用法:
开辟单变量地址空间
int *a = new int ; // 开辟一个存放数组的存储空间,返回一个指向该存储空间的地址. (和对象初始化一样:内置类型是否全局决定是否初始化,类类型无论如何都会调用默认构造函数来初始化)
int *a = new int(5) ; // 作用同上,但是同时将整数赋值为5
const int *a = new const int(5) ; // 作用同上,但是定义的是一个常量整数
开辟数组空间
一维: int *a = new int[100] ; // 开辟一个大小为100的整型数组空间
二维: int **a = new int[5][6] ;
三维及其以上:依此类推.
一般用法: new 类型 [初值]
Ø delete用法:
int *a = new int;
delete a; //释放单个int的空间
int *a = new int[5] ;
delete [] a ; //释放int数组空间
动态创建的对象不会被系统自动回收(即使动态变量已经超出了有效范围也不会)因此一定要手动 delete 如果忘记了该释放步骤系统内存可能会很快耗尽。
虽然回收了动态内存,但是这时候指针依然指向原来的地址叫做悬垂指针。如果在对该指针做操作可能会破坏内存数据。因此释放了数据空间后应该让指针指向 0 (a = 0)
4. 类型转换
分为隐式转换和显示转换
Ø隐式转换无需程序员介入系统自动实现转换例如:
char a ='a' ;
int b(a) ;
double c = b ;、
隐式转换常发生在小转大的同类类型之间,子类转父类,数组和对应指针,枚举到整形,任意结果到布尔值的转换等等。
Ø显示转换需要借助系统提供的转换函数来实现
static_cast、dynamic_cast、reinterpret_cast和const_cast
用法: static_cast < type-id > ( expression )
该运算符把expression转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
①用于类层次结构中基类和子类之间指针或引用的转换。
进行上行转换(把子类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
③把空指针转换成目标类型的空指针。
④把任何类型的表达式转换成void类型。
注意: static_cast不能转换掉expression的const、volitale、或者__unaligned属性。
2 dynamic_cast
用法: dynamic_cast < type-id > ( expression)
该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *;
如果type-id是类指针类型,那么expression也必须是一个指针,如果type-id是一个引用,那么expression也必须是一个引用。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换。
在类层次间进行上行转换时, dynamic_cast和 static_cast的效果是一样的;
在进行下行转换时, dynamic_cast具有类型检查的功能,比 static_cast更安全。
public:
int m_iNum;
virtual void foo();
};
class D: public B{
public:
char *m_szName[ 100];
};
void func(B *pb)
{
D *pd1 = static_cast(pb);
D *pd2 = dynamic_cast(pb);
}
在上面的代码段中,如果pb指向一个D类型的对象,pd1和pd2是一样的,并且对这两个指针执行D类型的任何操作都是安全的;
但是,如果pb指向的是一个B类型的对象,那么pd1将是一个指向该对象的指针,对它进行D类型的操作将是不安全的(如访问m_szName),而pd2将是一个空指针。
另外要注意: B要有虚函数,否则会编译出错;static_cast则没有这个限制。
这是由于运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表(关于虚函数表的概念,详细可见)中,只有定义了虚函数的类才有虚函数表,没有定义虚函数的类是没有虚函数表的。
另外,dynamic_cast还支持 交叉转换(cross cast)如下代码所示。
public:
int m_iNum;
virtual void
f(){}
};
class B: public A
{
};
class D: public A
{
};
void foo()
{
B *pb = new B;
pb->m_iNum = 100;
D *pd1 = static_cast(pb); // compile error
D *pd2 = dynamic_cast(pb); // pd2 is NULL
delete pb;
}
在函数foo中,使用 static_cast进行转换是不被允许的,将在编译时出错;而使用 dynamic_cast的转换则是允许的,结果是空指针。
3 reinpreter_cast
用法: reinpreter_cast (expression)
type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,在把该整数转换成原类型的指针,还可以得到原先的指针值)。
该运算符的用法比较多。
4 const_cast
用法: const_cast (expression)
该运算符用来修改类型的const或volatile属性。除了const或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;
常量引用被转换成非常量引用,并且仍然指向原来的对象;常量对象被转换成非常量对象。
Voiatile和const类试。举如下一例:
{
public:
int m_iNum;
}
void foo()
{
const B b1;
b1.m_iNum = 100; // comile error
B b2 = const_cast(b1);
b2. m_iNum = 200; // fine
}
使用 const_cast 把它转换成一个变量对象,就可以对它的数据成员任意改变。
C++ Primer 第六章 语句
C++ Primer 第七章 函数
7.1 函数的定义
函数是完整的一个逻辑代码块。 函数接收一些参数并在函数体内做处理。函数可以返回运算结果也可能不返回任何内容。
C++有两类函数 无返回值的void函数 和 有返回值函数。
7.2 参数传递
Ø非引用形参
参数是以复制形式传递的,函数内部对参数修改不一会对调用函数的实参产生影响。看下面代码
{
i = 3;
s = "new";
}
int it = 2;
string str = "old";
cout << it << str << endl; // 输出: 2 old
funct(it, str);
cout << it << str << endl; // 输出还是: 2 old
调用函数funct时传递的是实参的拷贝,虽然在函数内更改了参数值但不影响外部实参。
需要注意的是指针。在函数内对指针参数本身(指针的指向地址)的更改同样不会影响调用实参。不过对指针指向的内容更改却是有影响的。
{
*i = 3; // 对指向内容做更改
}
int it = 2;
string itpr = ⁢
cout << itpr << *itpr << endl; // 输出: 000ffxx0 2
funct(itpr);
cout << itpr << *itpr << endl; // 输出: 000ffxx0 3
函数对指针本身做的更改(指向地址)不会反应到外部实参。 指向内容做更改对外部实参是有影响的,实际上两个指针指向同一内容,任意一个指针所做的更改对其他指针都有反应。
函数体内两个表达式顺序执行对结果有很大影响。若先改变指针指向再改变指向内容值,实际上就表示两个指针指向了不同对象,这时修改指向内容对外部实参没有丝毫影响。
有些时候我们希望传递的参数不允许修改,在参数前增加 const 关键字就可以实现。
void function(const int i)
{
i = 2 ; // 错误,不允许修改常量参数
}
指针比较特殊。指针操作有两个含义:指针指向更改和指向内容更改。这个常量修饰符实际修饰的是指向内容,指向地址还是可以更改的 如下代码:
void function(const int *i)
{
*i = 2 ; // 错误,不允许更改指向内容
i = 0 ; // 允许更爱指针指向地址
}
Ø引用形参
非引用传参方式有个很大的弊端,当需要传递一个很大的参数时拷贝参数副本开销比较大。如果我们要求函数对参数修改能反映到外部的实参时非引用方式也无能为力。
这个时候可以使用引用类型传参。
{
i = 0 ;
j = 0 ; // 不允许更改
}
int it = 2;
int jt = 1;
cout << it << jt << endl; // 输出: 2 1
funct(it, jt);
cout << it << jt << endl; // 输出: 0 1
it以引用方式传递,函数内部修改 i 实际上就是在修改 it 本身。 如果限制函数更改可以增加 const 修饰。j 参数经过修饰后内部不允许做修改。
对于指针来说指针引用(*&parname)形参在函数体内指向更改或者指向内容更改都会反映到外部实参:
{
p = 1100ffxx ; // 修改指向
*p = 6 ; // 修改指向内容 注意此时修改的是新地址的内容值
}
int it = 2;
int pr = ⁢
cout << pr << *pr << endl; // 输出: fff110xx 2
funct(pr);
cout << pr << *pr << endl; // 输出: 1100ffxx 6
函数形参可传递实参一般分为 字面值常量, 普通常量,变量。不同情况下可传递的情况不同
}
int i(1);
const int j(1);
funct(15); // 传递字面值
funct(i); // 传递变量
funct(j); // 传递常量
也可以const限定参数 void funct(const int p) 三类参数传递已然正确,唯一的区别是限定后函数体内不允许更改参数 p
非const引用形参:
void funct(int &p)
{
}
int i(1);
const int j(1);
funct(3); // 不允许传递字面值
funct(i); // 传递变量
funct(j); // 不允许,函数内部可以修改参数值,而参数是常量时修改操作是是非法的
const引用形参:
void funct(const int &p)
{
}
int i(1);
const int j(1);
funct(1); // 传递字面值
funct(i); // 传递变量
funct(j); // 传递常量
应该掌握每种情况下可传递的实参类型。
对于容器参数如果按拷贝传递性能会大大降低。可以传递引用。 但时间操作中传递容器的迭代器会更加方便。迭代器类似于数组指针。
数组也可以作为新参,不过数组比较特殊因为数组不允许赋值,所以数组新参会被转换为对应指针。
void funct(int *i) ; // 推荐写法,表明参数是一个指向数组的指针
void funct(int[]) ;
void funct(int[10]) ;
这三种方式是一样的,实际等价于 void funct(int *i)
第三种形参定义设置了数组大小,但是实际调用中可以传递大小不等于10的数组,因为形参定义的数组大小会被忽略。 所以在函数中的操作不能依赖形参定义的大小。
如果想严格匹配实参和形参数组大小可使用 引用数组,定义方式如下:
void funct(int (&arr)[10]) ; // 表示是一个数组引用,且数组大小是10 。 圆括号必须因为下标操作具有更高优先级
void funct(int &arr[10]) ; // 错误的语法
函数操作数组不但可以如上传递还能可以传递指针,数组大小等参数类型
void funct(int *bej, int *end)
{
for(int *i = bej; i != end; i++)
{
cout << *i << endl;
}
}
int it[]{1,9,7,0,1};
funct(it, it + 5); // 注意:末端哨兵位是最后一位的后一位
// 传递第一位和数组大小
void funct(int *bej, size_t len)
{
for(size_t i = 0; i < len; i++)
{
cout << bej[i] << endl; // 数组指针下标操作等价于 *(beg + i)
}
}
size_t len = 5;
int it[len]{1,9,7,0,1};
funct(it, len);
7.3 函数返回值
函数可以没有返回值(void函数)也可以有返回值。同参数一样函数可返回多种类型。
Ø返回非引用类型
这种情况和形参类似,返回的是对象副本。
{
return 1 ; // 返回字面值副本
return a ; // 返回参数副本
int b(1) ;
return b ; // 返回局部变量副本
}
Ø返回引用类型
返回引用表示可以对返回值做赋值操作。
int &funct(int &a)
{
return a;
}
int i(1);
int &j = funct(i); // 也可以这样操作 funct(i) = 3
cout << i << j << endl // 输出 3 3 j 是 i 的引用
// 可以返回const引用
const int &funct(int &a)
{
return a;
}
int i(1);
int &j = funct(i); // 错误,不能用const引用初始化非const引用
const int &j = funct(i);
j = 3; // 错误,不能为常量赋值
千万记住:不管任何时候都不允许用const引用初始化非const引用(或者说不允许让非 const 引用指向一个const对象)
返回值遵循一个安全约束,即 不允许返回局部对象的引用或指针
int &funct(int a, int &b)
{
int c = 1;
return c; // 标准局部对象 不允许返回
return a; // 传参时拷贝的实参,实际值只有在函数内有效,不允许返回
return b; // 通过引用形参 传递的是外部实参的引用,可以传递
}
// 返回指针
int *funct(int a, int *b)
{
int c = 1;
return *c; // 标准局部对象 不允许返回
return *a; // 传参时拷贝的实参,实际值只有在函数内有效, 不允许返回
return b; // 传参时拷贝的实参,虽然指针是局部对象但他指向的值是外部数据,可以传递
}
总之判断返回是否是局部变量可以看返回值的实际值是作用范围否只在函数体内。
7.4 函数声明
函数调用时还未定义则需要声明。函数声明和变量声明类似可查看相关内容。
函数形参可以定义默认值,调用函数时如果不提供参数值则使用默认值
{
}
funct() // 等同于 funct(1,"val", " ")
funct(3) // 等同于 funct(3,"val", " ")
funct(3,"new") // 等同于 funct(3, "new", " ")
7.5 局部对象
局部对象的作用范围限制在函数内,函数执行完毕局部变量会被系统自动销毁。
不过静态局部对象不会销毁,所有静态对象一旦创建就会一直存在直到应用程序关闭。
静态对象初始化只会执行一次:
{
static int a(0); // 只会执行一次,第二次执行到代码处会检查静态变量 a 是否存在若存在则跳过
a++;
cout << a << endl;
} // 函数到此执行完毕并清理局部对象,但不会清理静态对象,所以a依然存在,值为最后操作结果。 a虽然不会被清理但只限在定义的函数体内访问
funct() ; // 输出 1
funct() ; // 输出 2
funct() ; // 输出 2
7.6 内联函数
内联函数是指编译时将调用函数的地方用实际函数体语句替代的一类函数,函数定义时返回值前加上 inline 就表示函数内联
inline bool funct(int a, int`b)
{
return a > b;
}
bool c = funct(a, b) ; // 编译时实际会被替换为 bool c = (a > b);
内联函数有较好的性能,因为函数在调用时系统刚要分配栈空间,内联函数会直接展开代码,所以不会有栈空间分配步骤 (内联函数体内数据需要的空间会在调用它的函数执行时一并分配)。
内联函数的特点决定了如果修改函数则所有用到内联函数的地方都要重新编译。
7.7 类的成员函数
类成员函数和普通该函数没有本质区别。
类成员函数包含一个隐藏参数 this 指针。它指向成员函数所属的当前类对象。
类成员函数名后面可以用const修饰,其作用是表示 this 指针指向常量,也就是说不允许修改当前类的任何成员
{
public:
int a ;
void funct1() const
{
a = 1; // 错误,等价于 this->a = 1 由于函数后面const修饰 this 指针指向了常量所以无法修改
}
void funct2()
{
a = 1; // 允许,等价于 this->a = 1
}
}
类成员函数比较多,比如普通成员函数,构造函数,拷贝函数,析构函数等等,会在后面详解。
7.8 函数重载
函数重载是指返回类型相同,函数名相同但参数不完全相同的多个函数。函数调用时会根据传递的参数类型和个数寻找最合适的重载函数。
不但参数类型和个数可以作为重载依据,当形参数是引用或指针时 const 可用作重载依据。
// 引用形参时 是否是const
const int b;
通常来说函数以参数类型不同或参数数量来重载。
7.9 指向函数的指针
指针不但可以指向内置类型,类类型,数组,还可以指向函数。函数指针的最大作用是将函数作为一种类型传递。
定义语法是:
void (*pr)(int i, string s) ; // 定义了一个函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型
pr = funct ; // 为定义的指针赋值(funct是函数名称)
pr(1,"str") ; // 通过指针调用函数(不用解引)
可以用 typedef 可简化定义,将定义一个变为定义一类
typedef void (*pr)(int i, string s) ; // 定义了一类函数指针,指向的函数无返回值,并且有两个参数一个是int类型另外一个string类型
pr f1 = funct ; // 定义类型变量
f1(1,"str") ; // 通过指针调用函数
在这两种方式中,指针都可以赋0值,表示还未指向任何函数。
最后我们看看实际中函数指针的应用
// 形参是函数指针 返回函数指针
pr getfunction(pr f,int`a)
{
f(a); // 执行
return f; // 返回函数指针
}
pr f1 = getfunction(funct, 1);
f1(2);
定义参数时用了typedef的类型pr,如果不使用则定义语句相当复杂。
对于有重载的函数,定义的指针一定要有对应的重载版本。 初始化指针变量时系统也会查找对应的重载函数
void fun(string a);
typedef void (*pr)(string); // 定义了string形参版的函数指针
pr p = fun; // 定义函数指针变量,系统会用 void fun(string) 版函数来初始化p