第三章 处理数据
- 计算机程序在存储数据是必须跟踪的3中基本属性
1、信息存储在何处;
2、存储的值为多少;
3、存储的信息是什么类型。
- 强制类型转换的通用格式
- (typename)value 该方法来自c语言;
- typename (value) 该方法纯碎的C++,该想法是要让强制类型转换就像是函数调用;
- static_cast <typename> (value)可用将一种数值类型转换成为另外一种数值类型;
- auto声明
在初始化声明中,如果使用关键字auto,而不指定变量的类型,编译器将把变量类型设置成与初始值相同。
第四章 复合类型
数组
- 数组<typename arrayName[arraySize]> ——存储相同类型的一组数据
数组声明应注意以下三点
——存储每个元素的类型
——数组名
——数组中元素的个数(必须为整形常数或者const,也可以是常量表达式) - 将sizeof运算符用于数组名,得到的是整个数组中的字节数;将sizeof用于数组元素,则得到的将是元素的字节
字符串
-
字符串——存储在内存的连续字节中的一系列字符(C++处理字符串的方式有两种:一种来自C语言,另一种是基于string类库的方法)
——字符串的初始化方法
char bird[11] = “Mr.Cheeps”;(用引号括起来的字符串隐式的包含空字符串)
char bird[11] = {‘M’,‘r’,’.’,‘C’,‘h’,‘e’,‘e’,‘p’,‘s’,’\0’};
——在数组中使用字符串- 将数组初始化字符串常量;
- 将键盘或文件输入读入到数组
——cin使用空白(空格,制表符和换行符)来确定字符串的结束位置,这意味着cin在获取字符数组输入时只读取一个单词,读取该单词后,cin将该字符串放到数组中,并自动在结尾添加空字符。
每次读取一行字符串
istream中的类(如cin)提供了一些面向行的类成员函数:getline()和get()。这两个函数都读取一行输入,直到到达换行符。然而,随后**getline()将丢弃换行符,而get()**将换行符保留在输入序列中。
- cin.getline(),该函数有两个参数,第一个参数是用来存储输入行的数组的名称,第二个参数是要读取的字符数。
- cin.get(),该函数的使用和**cin.getline()**一样,只是换行符保留在输入队列中
假设用get()将一行读入数组中,如何知道停止读取的原因是由于已经读取了整行,而不是由于数组已经填满呢?
- 查看下一个输入字符,如果是换行符,说明已经读取了整行;否则,说明该行中还有其他输入。
空行和其他问题![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/e0abf466c6a92c29ac43b182db2b8e41.png)
string类简介
string类初始化
- 可以使用C风格字符串来初始化string对象;
- 可以使用cin 来将键盘输入存储到string对象中;
- 可以使用cout来显示string对象;
- 可以使用数组表示法来访问存储在string对象中的字符。
string类的赋值、拼接和附加
- 不能将一个数组赋给另一个数组,但可以将string类对象赋给另一个string类对象
- 可以使用运算符+将两个string对象合并起来,还可以使用+=将字符串附加到string对象的末尾。
结构简介(struct)
结构是用户定义的类型,而类型声明定义了这种类型数据属性。定义了类型后,便可以创建这种类型的变量。因此创建结构包括两步。首先,定义结构描述——它描述并标记了能够存储结构中的各种数据类型。然后按描述穿件结构变量(结构数据对象)。
C++允许声明结构体变量时省略关键字struct。
结构初始化
- 与数组一样,C++11也支持将列表初始化用于结构,且等号(=)是可选择的!!!;
- 如果大括号未包含任何东西,那么结构体将初始化为零;
- 列表中,不允许缩窄转换。
共用体(union)
共用体是一种数据格式,它能够存储不同的数据类型,但只能同时存储其中的一种类型。
- 由于共用体每次只能存储一个值,因此它必须有足够的空间来存储最大的成员,所以,共用体的长度为其最大成员的长度。
- 匿名共用体没有名称,其成员将位于相同地址处的变量
- 共用体常用于节省内存!!!
指针和存储空间
int* p1, p2
,其中p1是一个指针,p2是一个int变量。警告:一定要在对指针应用解除引用运算符(*)之前,将指针初始化为一个确定的、适当的地址。
使用new来分配内存
为一个数据对象(可以是结构,也可以是基本类型)获得并指定分配内存的通用格式如下:
typeName * pointer_name = new typeName;
需要在两个地方指定数据类型:用来指定需要什么样的内存和用来声明合适的指针。
- C++提供了检测并处理内存分配失败的工具
警告:只能用delete来释放使用new分配的内存,并且只能释放一次,否则结果将是未定义的。
- 为数组分配内存的通用格式如下:
type_name * pointer_name = new type_name [num_elements];
对数组应用sizeof
得到的是数组的长度,对指针应用sizeof
得到的是地址的长度
指针、数组和指针算术
数组的地址
数组名被解释为数组中第一个元素的地址,而对数组名应用地址运算符时,得到的是整个数组的地址。
数组的替代品
模板类vector
模板类array
循环和关系表达式
cout
在显示bool
值之前将它们转换为int
,但cout.serf(ios:: boolalpha)
函数调用设置了一个标记,该标记命令cout
显示true
和false
,而不是1和0。
y = (4 + x++) + (6 + x++);
C++ 没有规定实在计算每个子表达式之后将x的值递增,还是在整个表达式计算完毕后才将x的值递增,有鉴于此,应该避免使用这样的表达式。
注意(该程序的结果)
#include <iostream>
using namespace std;
int main()
{
char ch;
int count = 0;
cout <<" Enter characters; enter # to quit:\n";
cin >> ch;
while (ch != '#')
{
cout << ch;
++count;
cin >> ch;
}
cout << endl << count << " characters read\n";
return 0;
}
- 完整程序
#include <iostream>
using namespace std;
int main()
{
char ch;
int count = 0;
cout <<" Enter characters; enter # to quit:\n";
// cin >> ch;
cin.get(ch);
while (ch != '#')
{
cout << ch;
++count;
// cin >> ch;
cin.get(ch);
}
cout << endl << count << " characters read\n";
return 0;
}
分支语句和逻辑运算符
顺序点:或运算符(||)、冒号运算符(:)、逗号运算符(,)、与运算符(&&)
- 程序发现用户输入错误内容时,应采取3个步骤
1、重置cin以接受新的输入;
2、 删除错误输入;
3、 提示用户在输入。
演示程序如下:
#include <iostream>
const int Max = 5;
int main()
{
using namespace std;
int golf[Max];
cout << "Please enter your golf scores.\n";
cout << "You musr enter " << Max << "rounds.\n";
int i;
for (i = 0; i < Max; i++)
{
cout << "round #" << i+1 << ":";
while (!(cin >> golf[i])){//注意这个循环,出错处理!!!
cin.clear();
while (cin.get() != '\n')
continue;
cout << "Please enter a number: ";
}
}
double total = 0.0;
for (i = 0; i < Max; i++)
total += golf[i];
cout << total / Max << " = average score "
<< Max << " rounds\n";
return 0;
}
循环内部:
该循环的第一条语句使用clear()重置输入,如果省略这条语句,程序将拒绝继续读取输入。接下来,程序在while循环中使用cin.get()来读取行尾之前的所有输入,从而删除这一行中的错误输入。另一种方法是读取到下一个空白字符,这样讲每次删除一个单词,而不是一次删除整行。最后,程序告诉用户,应输入一个数字。
简单文件输入/ 输出
文件输出的主要步骤
- 包含头文fstream;
- 创建一个fstream对象;
- 将fstream对象与文本文件关联起来;
- 就像使用cout那样使用fstream对象。
演示程序
#include <iostream>
#include <fstream>
int main()
{
using namespace std;
char automobile[50];
int year;
double a_price;
double d_price;
ofstream outFile;//创建对象
outFile.open("carinfo.txt");//关联文件
cout << "Enter the make and model of automobile: ";
cin.getline(automobile,50);
cout << "Enter the model year: ";
cin >> year;
cout << "Enter the original asking price: ";
cin >> a_price;
d_price = 0.913 * a_price;
cout << fixed;//用一般的方式输出浮点型,例如C++程序在控制台显示大一点的数,显示的时候使用了科学计数法,使用该命令即可享像一般的方式显示。
cout.precision(2);//设置精度为2,并返回上一次的设置。
cout.setf(ios_base::showpoint);//显示浮点数小数点后面的零。
cout << "Make and model: "<< automobile << endl;
cout << "Year: "<< year << endl;
cout << "Was asking $" << a_price << endl;
cout << "Now asking $" << d_price << endl;
outFile << fixed;
outFile.precision(2);
outFile.setf(ios_base::showpoint);
outFile << "Make and model: "<< automobile << endl;
outFile << "Year: "<< year << endl;
outFile << "Was asking $" << a_price << endl;
outFile << "Now asking $" << d_price << endl;
outFile.close();
return 0;
}
检查文件
检查文件是否成功打开至关重要,下面是一些可能出问题的地方:
- 指定的文件可能不存在;
- 文件可能位于另一个目录中;
- 访问可能被拒绝;
- 用户可能输错了文件名或省略了文件扩展名。
检查文件是否被成功打开可避免将这种经历放在错误地方的情况发生
函数——C++的编程模块
函数原型的功能
函数和数组
演示程序
#include <iostream>
const int ArSize = 8;
int sum_arr(int arr[], int n);
int main()
{
int cookies[ArSize] = {1,2,4,8,16,32,64,128};
std::cout << cookies << " = array address, ";
std::cout << sizeof cookies << " = sizeof cookies\n";
int sum = sum_arr(cookies, ArSize);
std::cout << "Total cookies eaten: " << sum << std::endl;
sum = sum_arr(cookies, 3);
std::cout << "First three eaters ate " << sum << " cookies.\n";
sum = sum_arr(cookies + 4, 4);
std::cout << "Last four eaters ate " << sum << " cookies.\n";
return 0;
}
int sum_arr(int arr[], int n)
{
int total = 0;
std::cout << arr << " = arr, ";
std:: cout << sizeof arr << " = sizeof arr\n";//**注意,从这里可以看出sizeof arr 只是指针变量的长度;必须显示传递数组的长度,而不能通过sizeof arr来获取数组的程度**
for (int i = 0; i < n; i++)
total = total + arr[i];
return total;
}
保护数组
为防止函数无意间修改数组的内容,可在声明形参时使用关键字const修饰。
int age = 30;
const int * pt = &age;// 该声明只能防止修改pt指向的值,而不能防止修改pt的值;
int sage = 40;
pt = &sage; //修改pt是允许的,但仍然不能通过pt来修改sage的值;
//下面这种方法无法修改指针的值
int sloth = 3;
const int * ps = &sloth; //ps是指向整形常量的指针
int * const finger = &sloth; // finger 是一个常指针指向整形变量,允许通过finger指针修改sloth;
//下面这种声明是指向const对象的const指针
double trouble = 2.0E30;
const double * const stick = &trouble;//stick是一个常指针,指向的是一个常量,
//不允许修改stick指针,不允许通过stick指针修改 trouble变量;
函数和结构
- 当结构比较小时,按值传递结构较为合理
- 传递结构的地址可以节省时间和空间,和传递函数的数组名一样。
示例程序
#include <iostream>
#include <cmath>
struct polar
{
double distance;
double angle;
};
struct rect
{
double x;
double y;
};
void rect_to_polar (const rect * pxy, polar * pda);
void show_polar (const polar * pda);
int main()
{
using namespace std;
polar pplace;
rect rplace;
cout << "Enter the x and y values: ";
while (cin >> rplace.x >> rplace.y)
{
rect_to_polar(&rplace, &pplace);
show_polar(&pplace);
cout << "Next two numbers (q to quit): ";
}
cout << "Done.\n";
return 0;
}
void rect_to_polar (const rect * pxy, polar * pda)
{
pda->distance = sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
pda->angle = atan2(pxy->y, pxy->x);
}
void show_polar (const polar * pda)
{
using namespace std;
const double Rad_to_deg = 57.29577951;
cout << "distance = " << pda->distance;
cout << ", angle = " << pda->angle * Rad_to_deg;
cout << " degrees\n";
}
- 模板array并非只能存储基本数据类型,它还能存储类对象
示例程序
#include <iostream>
#include <array>
#include <string>
const int Seasons = 4;
const std::array<std::string, Seasons> Snames =
{ "Spring","Summer","Fall","Winter"};
// function to modify array object
void fill (std::array<double, Seasons> *pa);
// function that uses array object without modifying it
void show (const std::array<double, Seasons> da);
int main()
{
std::array<double, Seasons> expenses;
fill(&expenses);
show(expenses);
return 0;
}
void fill (std::array<double, Seasons> *pa)
{
using namespace std;
for (int i = 0 ; i < Seasons ; i++)
{
cout << "Enter "<<Snames[i] << " expenses: ";
cin >> (*pa)[i];
}
}
void show (const std::array<double, Seasons> da)
{
using namespace std;
double total = 0.0;
cout << "\nEXPENSES\n";
for (int i = 0; i < Seasons; i++)
{
cout << Snames[i] << ": $" << da[i] << endl;
total += da[i];
}
cout <<"Total Expenses: $" << total << endl;
}
递归1
#include <iostream>
void countdown (int n);
int main()
{
countdown (4);
return 0;
}
void countdown (int n)
{
using namespace std;
cout << "Counting down ..." << n << endl;
if (n > 0)
countdown ( n-1);
cout << n << ":Kaboom!\n";
}
递归2
#include <iostream>
const int Len = 66;
const int Divs = 6;
void subdivide (char ar[], int low, int high, int level);
int main()
{
char ruler[Len];
int i;
for (i = 1; i < Len -2; i++)
ruler[i] = ' ';
ruler[Len - 1] = '\0';
int max = Len - 2;
int min = 0;
ruler[min] = ruler[max] = '|';
std::cout << ruler << std::endl;
for (i = 1; i <= Divs; i++)
{
subdivide(ruler,min,max,i);
std::cout << ruler << std::endl;
for (int j = 1; j < Len -2; j++)
ruler[j] = ' ';
}
return 0;
}
void subdivide (char ar[], int low, int high ,int level)
{
if (level == 0)
return ;
int mid = (high + low)/2;
ar[mid] = '|';
subdivide (ar, low, mid, level - 1);
subdivide (ar, mid, high, level - 1 );
}
函数指针
- 获取函数的地址
只要使用函数名即可。(也就是说,如果think()是一个函数,则think就是该函数的地址) - 通常,要声明指向特定类型的函数的指针,可以首先编写这种函数的原型,然后用(*pf)替换函数名。这样pf就是这类函数的指针。
double (*pf) (int); // pf points to a function that returns double
double *pf (int); // pf() a function that returns a pointer-to-double
程序示例
#include <iostream>
double betsy(int);
double pam(int);
void estimate(int lines, double (*pf)(int));
int main()
{
using namespace std;
int code;
cout << "How many lines of code do you need? ";
cin >> code;
cout << "Here's Betsy's estimate:\n";
estimate(code, betsy);
cout << "Here's Pam's estimate:\n";
estimate(code, pam);
return 0;
}
double betsy(int lns)
{
return 0.05 * lns;
}
double pam(int lns)
{
return 0.03 * lns + 0.0004 * lns * lns;
}
void estimate(int lines, double (*pf)(int))
{
using namespace std;
cout << lines << "lines will take ";
cout << (*pf)(lines) << "hour(s)\n";
}
函数探幽
内联函数
- 在函数声明前加上关键字inline;
- 在函数定义前加上关键字inline;
通常的做法是省略函数原型,直接将函数定义放在本应提供原型的地方
内联函数不能递归
引用变量
- 引用是已定义变量的别名
- 引用变量的主要用途是用作函数的形参 ,通过将引用变量用作参数,函数将使用原始数据,而不是其副本。
- 引用必须在声名时将其初始化
示例程序
#include <iostream>
int main()
{
using namespace std;
int rats = 101;
int & rodents = rats;
cout << "rats = " << rats;
cout << ", rodents = "<< rodents << endl;
cout << "rats address = " << &rats;
cout << ", rodents address = " << &rodents <<endl;
int bunnies = 50;
rodents = bunnies; //注意此处?有点唬人!!! 思考
cout << "bunnies = " << bunnies;
cout << ", rats = " << rats;
cout << ", rodents = " << rodents << endl;
cout << "bunnies address = " << &bunnies;
cout << ", rodents address = " << &rodents << endl;
return 0;
}
示例程序
#include <iostream>
void swap1(int a, int b);
void swap2(int & a, int & b);
void swap3(int * pa, int * pb);
int main()
{
using namespace std;
int aa = 5;
int bb = 9;
swap1(aa, bb);
cout << "aa = " << aa;
cout << ", bb = " << bb << endl;
int aaa = 4;
int bbb = 6;
swap2(aaa, bbb);
cout << "aaa = " << aaa;
cout << ", bbb = " << bbb << endl;
int ab = 5;
int ba = 3;
swap3(&ab, &ba);
cout << "ab = " << ab;
cout << ", ba = " << ba << endl;
return 0;
}
void swap1(int a, int b)
{
int temp ;
temp = a;
a = b;
b = temp;
}
void swap2(int & a, int & b)
{
int temp;
temp = a;
a = b;
b = temp;
}
void swap3(int * pa, int * pb)
{
int temp;
temp = *pa;
*pa = *pb;
*pb = temp;
}
示例程序
注意变量x的变化!!!
#include <iostream>
double cube (double a);
double refcube (double & ra);
int main()
{
using namespace std;
double x = 3.0;
cout << cube (x);
cout << " = cube (x) of " << x << endl;
cout << refcube (x) ;
cout << " = refcube (x) of " << x << endl;
return 0;
}
double cube (double a)
{
a *= a * a;
return a;
}
double refcube (double & ra)
{
ra *= ra * ra;
return ra;
}
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)
程序示例
#include <iostream>
#include <string>
struct free_throws
{
std::string name;
int made;
int attempts;
float percent;
};
void display (const free_throws & ft);
void set_pc (free_throws & ft);
free_throws & accumulate (free_throws & target, const free_throws & source);
int main()
{
free_throws one {"Ifelsa Branch", 13, 14};
free_throws two {"Andor Knott", 10, 14};
free_throws three {"Minnie Max", 7, 9};
free_throws four {"Whily Looper", 5, 9};
free_throws five {"Long Long", 6, 14};
free_throws team {"Throwgoods", 0, 0};
free_throws dup;
set_pc(one);
display(one);
accumulate(team, one);
display(team);
display(accumulate(team, two));
accumulate(accumulate(team,three), four);
display(team);
dup = accumulate(team, five);
std::cout << "Displaying team:\n";
display(team);
std::cout << "Displaying dup after assignment:\n";
display(dup);
set_pc(four);
accumulate(dup,five) = four;
std::cout << "Displaying dup after ill-advised addignment:\n";
display(dup);
return 0;
}
void display(const free_throws & ft)
{
using std::cout;
cout << "Name: " << ft.name << '\n';
cout << " Made: " << ft.made << '\t';
cout << "Attempts: " << ft.attempts <<'\t';
cout << "percent: " << ft.percent <<'\n';
}
void set_pc (free_throws & ft)
{
if (ft.attempts != 0)
ft.percent = 100.0f*float(ft.made)/float(ft.attempts);
else
ft.percent = 0;
}
free_throws & accumulate (free_throws & target, const free_throws & source)
{
target.attempts += source.attempts;
target.made += source.made;
set_pc(target);
return target;
}
返回引用时应注意的问题
返回引用时最重要的一点是,应避免返回函数终止时不再存在的内存单元引用。应该尽量避免下面这样的代码
const free_throws & clone2 (free_throws & ft)
{
free_throws newguy; // first step to big error
newguy = ft; // copy info
return newguy; // return reference to copy
}
该函数返回一个指向临时变量(newguy)的引用,函数运行完毕后它将不再存在。
为避免这种问题,最简单的方法是,返回一个作为参数传递给函数的引用。作为参数的引用将指向调用函数使用的数据,因此返回的引用也将指向这些数据。
- 左值——可以通过地址访问的值
- 右值——不可以通过地址访问的值
何时使用引用参数
使用引用参数的主要原因有两个:
- 程序员能够修改调用函数中的数据对象。
- 通过传递引用而不是整个数据对象,可以提高程序的运行速度。(当数据对象较大是《如结构和类对象》,第二个原因最重要。)
什么时候使用引用、什么时候应使用指针、什么时候按值传递呢?
默认参数
怎样设置?
必须通过函数原型。由于编译器通过查看原型来了解函数所使用的的参数数目,因此函数原型也必须将可能的默认参数告知程序。方法是将值赋给原型中的参数。例如,left()
的原型如下:
char * left (const char * str ,int n = 1);
- 对于带参数列表的函数,必须从右向左添加默认值。也就是说,要为某个参数设置默认值,则必须为它邮编的所有参数提供默认值:
int harpo (int n, int m = 4, int j = 5); //有效
int chico (int n, int m = 6, int j); // 无效
int groucho (int k = 1, int m = 2, int n = 3); // 有效
beeps = harpo (2); //same as harpo(2,4,5)
beeps = harpo (1,8); // same as harpo(1,8,5)
beeps = harpo (8,7,6); // 没有使用默认值
程序示例
#include <iostream>
unsigned long left (unsigned long num, unsigned ct);
char * left (const char * str, int n = 1);
int main()
{
using namespace std;
char * trip = "Hawaii!!";
unsigned long n = 12345678;
int i;
char * temp;
for (i = 1; i < 10; i++)
{
cout << left(n,i) << endl;
temp = left(trip,i);
cout << temp << endl;
delete [] temp;
}
return 0;
}
unsigned long left (unsigned long num, unsigned ct)
{
unsigned digits = 1;
unsigned long n = num;
if (ct == 0 || num == 0)
return 0;
while (n /= 10)
digits++;
if (digits > ct)
{
ct = digits - ct;
while (ct--)
num /= 10;
return num;
}
else
return num;
}
char * left (const char * str, int n)
{
if (n < 0)
n = 0;
char * p = new char[n+1];
int i;
for (i = 0; i < n && str[i]; i++)
p[i] = str[i];
while (i <= n)
p[i++] = '\0';
return p;
}
函数模板
如果需要多个将同一种算法用于不同类型的函数,请使用模板。如果不考虑向后兼容的问题,并愿意键入较长的单词,则声明类型参数时,应使用关键字typename而不使用class。
示例程序
#include <iostream>
template <typename T>
void Swap(T &a,T &b);
int main()
{
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j <<".\n";
cout << "Using compoler-generated int swapper:\n";
Swap(i,j);
cout << "Now i, j = " << i << ", " << j <<".\n";
double x = 24.5;
double y = 81.7;
cout << "x, y = " << x <<", " << y << ".\n";
cout << "Using compiler-gererated double swapper:\n";
Swap(x,y);
cout << "Now x, y = " << x <<", "<< y << ".\n";
return 0;
}
template <typename T> // or class T
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
示例程序(重载的模板)
和常规重载一样,被重载的模板的函数特征标必须不同,并非所有的模板参数必须都是模板参数类型
#include <iostream>
template <typename T>
void Swap(T &a,T &b);
template <typename T>
void Swap(T * a, T * b, int n);//注意此处
void Show (int a[]);
const int Lim = 8;
int main()
{
using namespace std;
int i = 10;
int j = 20;
cout << "i, j = " << i << ", " << j <<".\n";
cout << "Using compoler-generated int swapper:\n";
Swap(i,j);
cout << "Now i, j = " << i << ", " << j <<".\n";
int d1[Lim] = {0,7,0,4,1,7,7,6};
int d2[Lim] = {0,7,2,0,1,9,6,9};
cout << "Original arrays:\n";
Show(d1);
Show(d2);
Swap(d1,d2,Lim);
cout << "Swapped arrays:\n";
Show(d1);
Show(d2);
return 0;
}
template <typename T> // or class T
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <typename T>
void Swap(T * a, T * b, int n)
{
T temp;
for (int i = 0; i < n; i++)
{
temp = a[i];
a[i] = b[i];
b[i] = temp;
}
}
void Show (int a[])
{
using namespace std;
cout << a[0] << a[1] << "/";
cout << a[2] << a[3] << "/";
for (int i = 4; i < Lim ; i++)
cout << a[i];
cout << endl;
}
示例程序 (显示具体化)
编译器在选择原型时,非模板版本优先于显示具体化和模板版本,而显示具体化优先于使用模板生成的版本。
#include <iostream>
template <typename T>
void Swap(T &a, T &b);
struct job
{
char name[40];
double salary;
int floor;
};
template <> void Swap<job> (job & j1, job & j2);
void Show (job & j);
int main()
{
using namespace std;
cout.precision(2);
cout.setf(ios::fixed, ios::floatfield);
int i = 10, j = 20;
cout << "Using compiler-generates int swapper:\n";
Swap(i,j);
cout << "Now i, j = "<< i << ", " << j <<".\n";
job sue {"Susan Yaffee",73000.60, 7};
job sidney {"Sidney Taffee", 78060.72, 9};
cout << "Before job swapping:\n";
Show(sue);
Show(sidney);
Swap(sue,sidney);// sues void Swap(job &, job & ) 调用显示具体化函数
cout << "After job swapping: \n";
Show(sue);
Show(sidney);
return 0;
}
template <typename T>
void Swap(T &a, T &b)
{
T temp;
temp = a;
a = b;
b = temp;
}
template <> void Swap <job> (job &j1, job &j2)
{
double t1;
int t2;
t1 = j1.salary;
j1.salary = j2.salary;
j2.salary = t1;
t2 = j1.floor;
j1.floor = j2.floor;
j2.floor = t2;
}
void Show (job & j)
{
using namespace std;
cout << j.name << ": $ " << j.salary
<< " on floor " << j.floor << endl;
}
auto h (int x, float y) -> double;// 其中 -> double 被称为后置返回类
- decltype在参数声明后面,因此x和y位于作用域内,可以使用它们。
template <typename T1, typename T2>
auto gt(T1 x,T2 y) -> decltype(x + y)
{
...
return x + y;
}
内存模型和名称空间
说明符和限定符
cv限定符
- cons
- volatile 改善编译器的优化能力
mutable
可以用它来指出,即使结构或类变量为const,其某个成员也可以被修改。
struct data
{
char name[30];
mutable int accesses;
...
};
const data veep {"Claybourne Clodde", 0, ...};
strcpy(veep.name, "Joye Joux"); // not allowed
veep.accesses++; // allowed
传统的C++名称空间
名称空间的概念
- 声明区域:声明区域是可以在其中进行声明的区域。例如,可以在函数外面声明全局变量,对于这种变量,其声明区域为其声明所在的文件。对于在函数声明的变量,其声明区域为其生命所在的代码块。
- 潜在作用域:变量的潜在作用域从声明点开始,到其声明区域的结尾。因此潜在作用域比声明区域小,这是由于变量必须定义后才能使用。
- 作用域:变量对程序而言可见的范围被称为作用域(在函数中声明的局部变量,将隐藏在同一个文件中声明全局变量)
程序示例(名称空间)
- 头文件
namesp.h
//namesp.h
#include <string>
// creat the pers and debts namespaces
namespace pers
{
struct Person
{
std::string fname;
std::string lname;
};
void getPerson(Person &);
void showPerson(const Person &);
}
namespace debts
{
using namespace pers;
struct Debt
{
Person name;
double amount;
};
void getDebt(Debt &);
void showDebt(const Debt &);
double sumDebts(const Debt ar[], int n);
}
- 源代码文件
namesp.cpp
//namesp.cpp -- namespaces
#include <iostream>
#include "namesp.h"
namespace pers
{
using std::cout;
using std::cin;
void getPerson(Person & rp)
{
cout << "Enter first name: ";
cin >> rp.fname;
cout << "Enter last name: ";
cin >> rp.lname;
}
void showPerson(const Person & rp)
{
std::cout << rp.lname << ", " << rp.fname;
}
}
namespace debts
{
void getDebt(Debt & rd)
{
getPerson(rd.name);
std::cout << "Enter debt: ";
std::cin >> rd.amount;
}
void showDebt(const Debt & rd)
{
showPerson(rd.name);
std::cout << ": $" << rd.amount << std::endl;
}
double sumDebts(const Debt ar[], int n)
{
double total = 0;
for (int i = 0; i < n; i++)
total += ar[i].amount;
return total;
}
}
- 源代码文件
usenmsp.cpp
//usenmsp.cpp -- using namespaces
#include <iostream>
#include "namesp.h"
void other(void);
void another(void);
int main(void)
{
using debts::Debt;
using debts::showDebt;
Debt golf = {{"Benny", "Goatsniff"}, 120.0};
showDebt(golf);
other();
another();
return 0;
}
void other(void)
{
using std::cout;
using std::endl;
using namespace debts;
Person dg ={"Doodles","Glister"};
showPerson(dg);
cout << endl;
Debt zippy[3];
int i;
for ( i = 0; i < 3; i++)
getDebt(zippy[i]);
for ( i = 0; i < 3; i++)
showDebt(zippy[i]);
cout << "Total debt: $" <<sumDebts(zippy, 3) << endl;
return;
}
void another(void)
{
using pers::Person;
Person collector = {"Milo", "Rightshift"};
pers::showPerson(collector);
std::cout << std::endl;
}
第10章 对象和类
构造函数和析构函数
- 假设Bozo类的构造函数的原型如下:
Bozo(const char * fname, const char *lname);//constructor prototyp
- 则可以使用它类初始化对象:
Bozo bozetta = bozo("Bozetta", "Biggens"); // peimary form
Bozo fufu("Fufu","O'Dweeb"); //short form
Bozo *pc = new Bozp("Popo", "Le Peu"); //dynamic object
//如果编译器支持C++11,则可使用列表初始化:
Bozo bozetta = {"Bozetta", "Biggens"};
Bozo fufu{"Fufu","O'Dweeb"};
Bozo *pc = new Bozo{"Popo","Le Peu"};
警告: 接收一个参数的构造函数允许使用复制语法将对象初始化为一个值。
this指针
const Stock & topval(const Stock & s) const;
——该函数隐式地访问一个对象,而显示地访问另一个对象,并返回其中一个对象的引用。括号中的const表明,该函数不会修改被显示地访问的对象;而括号后的const表明,该函数不会修改被隐式地访问的对象。由于该函数返回可两个const对象之一的引用,因此返回类型也应为const引用。
const Stock & topval(const Stock & s) const
{
if (s.total_val > total_tal)
return s;
else
return *this;
}
//返回类型为引用意味着返回的是调用对象本身,而不是其副本。
- 每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。
类作用域
class Bakery
{
private:
const int Months = 12; //declart a constant ? FAILS错的
double costs[Months];
...
}
注意:const int Months = 12,是行不通的,因为声明类只是描述了对象的形式,并没有创建对象。
因此,在创建对象前,将没有用于存储值的空间。然而,有两种方式可以实现这个目标,并且效果相同!!!
作用域内枚举
第11章 使用类
运算符重载
- 要重载运算符,须使用被称为运算符函数的特殊函数形式。运算符函数的格式如下:
operatorop(argument-list);//其中op是C++运算符
Time coding , fixing; //其中Time是一个类
...
total = coding.operator+(fixing);//1
total = coding + fixing; //2
注意:这两种表示法都将调用operator+()方法。注意,在运算符表示法中,运算符左侧的对像(这里为coding)是调用对象,运算符右边的对象(这里
fixing)是作为参数被传递的对象
重载限制
友元
友元
- 友元函数
- 友元类
- 友元成员函数
通过让函数称为类的友元,可以赋予该函数与类的成员函数相同的访问权限。
Time operator*(double m, const Time & t);
- 对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数。
创建友元
步骤
——将其原型放在类声明中,并在原型声明前加上关键字friend:
friend Time operator*(double m, const Time & t);
- 虽然
operator*()
函数在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用; - 虽然
operator*()
函数不是成员函数,但它与成员函数的访问权限相同。
——编写函数定义。因为它不是成员函数,所以不要使用Time::
限定符。另外,不要再定义中使用关键字friend
重载<< 运算符
类的自动转换和转换函数
第12章 类和动态内存分配
- 将成员声明为静态存储类,静态类成员有一个特点:无论创建了多少对象,程序都只创建了一个静态类变量副本。也就是说,类的所有对象共享同一个静态成员,就像家中的电话可供全体家庭成员共享一样
strlen()
函数返回字符串的长度,但不包括末尾的空字符串。
静态类成员函数
- 静态成员函数中,函数声明必须包含关键字static
警告
——不能通过对象调用静态成员函数(静态成员函数不能使用this指针)
——如果静态成员函数实在共有部分声明的,则可以使用类名和作用域解析运算符来调用它。
——静态成员函数不与特定的对象相关联,因此只能使用静态数据成员。
有关返回对象的说明
返回指向const对象的引
Vector force1(50,60);
Vector force2(10,70);
Vector max;
max = Max(force1,force2);
下面两种实现都是可行的:
//version1
Vector Max(const Vector & v1, const Vector & v2)
{
if(v1.magval() > v2.magval())
return v1;
else
return v2;
}
//version2
const Vector Max(const Vector & v1, const Vector & v2)
{
if(v1.magval() > v2.magval())
return v1;
else
return v2;
}
说明:首先,返回对象将调用复制构造函数,而返回引用不会。因此,第二个版本所做的工作更少,效率更高。其次,引用指向的对象应该在调用函数执行时存在。在这个例子中,引用指向force1
或force2
,它们都是在调用函数中定义的,因此满足这种条件。第三,v1
和v2
都被声明为const
引用,因此返回类型必须为const
,这样才匹配。
返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用方式返回它,因为在被调用函数执行完毕时,局部对象将调用其析构函数。因此,当控制权回到调用函数时,引用指向的对象将不再存在。在这种情况下,应返回对象我不是引用。
嵌套结构和类
在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型;如果是在共有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。
- 对于
const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化
。C++提供了一种特殊的语法来完成上述工作,它叫做成员初始化列表。 - 对于声明为引用的类成员,也必须使用初始化列表 。
第13章 类继承
- 派生类对象存储了基类的数据成员(派生类继承了基类的实现);
- 派生类对象可以使用基类的方法(派生类继承了基类的接口);
- 派生类需要自己的构造函数;
- 派生类可以根据需要添加额外的数据成员和成员函数。
——派生类构造函数必须使用基类的构造函数
- 基类指针可以在不进行显示类型转换的情况下指向派生类对象
- 基类引用可以在不进行显示类型转换的情况下引用派生类对象
- 基类指针或引用只能用于调用基类方法,但是不可以将基类对象和地址赋给派生类引用和指针
静态联编和动态联编
函数名联编
:将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。- 在编译过程中进行进行联编被称为
静态联编
(早起联编) - 编译器生成能够在程序运行时选择正确的虚方法的代码,这被称为
动态联编
(晚期联编)
指针和引用类型的兼容性
- 将派生类引用或指针转换为基类引用或指针被称为
向上强制转换
,公有继承不需要显示类型转换
。
注意:Brass是基类,BrassPlus是派生类
BrassPlus dilly ("Annie Dill", 493222, 2000);
Brass * pb = &dilly;
Brass & rb = dilly;
- 将基类指针或引用转换为派生类指针或引用被称为
向下强制转换
,如果不使用显示类型转换,向下强制转换是不允许的。
原因是is-a关系式不可逆的!!
虚成员函数和动态联编
- 编译器对非虚方法使用静态联编
- 编译器对虚方法使用动态联编
有关虚函数的注意事
访问控制protected
private和protected之间的区别只有在基类派生的类中才会表现出来。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。
抽象基类(ABC)
- 纯虚函数:C++通过纯虚函数提供未实现的函数,纯虚函数声明的结尾出为
“=0”
当类声明中包含纯虚函数时,则不能创建该类的对象。
包含纯虚函数的类只用做基类。要成为真正的ABC,必须至少包含一个纯虚函数。
第14章 C++中的代码重用
valarray
- valarray类是由头文件valarray支持的。
- 模板类特性意味着声明对象时,必须指定具体的数据类型。因此,使用valarray类来声明一个对象时,需要标识符valarray后面加上一对尖括号,并在其中包含所需的数据类型。
valarray<int> q_values; // an array of int
valarray<double> weights; // an array of double
私有继承
私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。
私有继承与包含的主要区别
1、 包含版本提供了显示命名的对象成员,而私有继承提供了两个无名称的子对象成员。这是这两种方法的第一个主要区别。
2、
多重继承
虚基类使得从多个类派生出的对象只继承一个基类对象。
第十五章
类型转换运算符
第16章 string类和标准模板库
智能指针的使用
#include <iostream>
#include <string>
#include <memory>
class Report
{
private:
std::string str;
public:
Report (const std::string s): str(s)
{
std::cout << "Object created!\n";
}
~Report () {
std::cout << "Object deleted!\n";
}
void comment() const
{
std::cout << str << "\n";
}
} ;
int main()
{
{
std::auto_ptr<Report> ps (new Report("using auto_ptr"));
ps->comment();
}
{
std::shared_ptr<Report> ps (new Report("using shared_ptr"));
ps->comment();
}
{
std::unique_ptr<Report> ps (new Report("using unique_ptr"));
ps -> comment();
}
return 0;
}
- 如果程序要使用多个指向同一个对象的指针,应该选择
shared_ptr
。这样的情况包括:有一个指针数组,并使用一些辅助指针来表示特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL容器包含指针。 - 如果程序不需要多个指向同一个对象的指针,则可使用
unique_ptr
。如果函数使用new
分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr
是不错的选择。这样,将所有权将转让给接收返回值的unique_ptr
,而该智能指针将负责调用delete
。可将unique_ptr
存储到STL容器中,只要不调用将一个unique_ptr
赋值或赋给另一个的方法或算法。
泛型编程
- 为区分++运算符的前缀版本和后缀版本,C++将
operator++
作为前缀版本,将operator++(int)
作为后缀版本;其中的参数永远也不会被用到,所以不必指定其名称。
函数对象
生成器
是不用参数就可以调用的函数符。一元函数
是用一个参数可以调用的函数符。二元函数
是用两个参数可以调用的函数符。- 返回bool值的一元函数是
谓词
; - 返回bool值的二元函数是
二元谓词
。
第十七章 输入、输出和文件
使用cout进行格式化
- 如何设置显示整数时使用的计数系统?
要控制整数以十进制、十六进制和八进制显示,可以使用dec、hex和oct
控制符。例如下面的函数调用将cout对象的计数系统格式状态设置为十六进制:
hex(cout);
cout << hex;与上面语句等价!!!
- 调整字段宽度
可以使用width成员函数将长度不同的数字放到宽度相同的字段中,该方法的原型为:
int width();
该格式返回字段宽度的当前设置。
int width(int i);
该种格式将字段宽度设置为i个空格,并返回以前的字段宽度值。
width() 方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值。
例程
#include<iostream>
int main()
{
using std::cout;
int w = cout.width(30);
cout << "default field width = " << w << ":\n";
cout.width(5);
cout << "N" << ':';
cout.width(8);
cout << "N * N" << ":\n";
for (long i = 1; i <= 100; i *= 10)
{
cout.width(5);
cout << i <<':';
cout.width(8);
cout << i*i << ":\n";
}
return 0;
}
- 填充字符
在默认情况下,cout用空格填充字段中未被使用的部分,可以用fill()成员函数来改变填充字符。该函数一旦设置将一直有效,直到被重新设置。
cout.fill('*')
- 设置浮点数的显示精度
在默认模式下,它指的是显示的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。
已经知道,C++的默认精度为6位(但末尾的0将不显示)。precision()成员函数使得能够选择其他值。例如,下面的语句将cout的精度设置为2:
cout.precision(2);
和width()的情况不同,但与fill()类似,新的精度设置将一直有效,知道被重新设置。
#include <iostream>
int main()
{
using std::cout;
float price1 = 20.40;
float price2 = 1.9 + 8.0 / 9.0;
cout << "\"Furry Friends\" is $" << price1 << "!\n";
cout << "\"Fiery Fiends\" is $" << price2 << "!\n";
cout.precision(2);
cout << "\"Furry Friends\" is $" << price1 << "!\n";
cout << "\"Fiery Fiends\" is $" << price2 << "!\n";
return 0;
}
- setf()
例程
#include <iostream>
int main()
{
using std::cout;
using std::endl;
using std::ios_base;
int temperature = 63;
cout << "Today's water temperature: ";
cout.setf(ios_base::showpos);//在正数前面加上+;
cout << temperature << endl;
cout << "For our programming friends, that`s\n";
cout << std::hex << temperature << endl;
cout.setf(ios_base::uppercase);//对于十六进制的输出,使用大写字母E表示法,
cout.setf(ios_base::showbase);//对于输出,使用C++基数前缀(0或者0x)
cout << "or\n";
cout << temperature << endl;
cout << "How " << true <<"! oops -- How ";
cout.setf(ios_base::boolalpha);//输入输出可以是bool值(false或者true)
cout << false << "!\n";
return 0;
}
使用cin进行输入
流状态
文件的输入和输出
第十八章
复习内容
例程
#include <iostream>
#include <initializer_list>
double sum(std::initializer_list<double> il);
int main()
{
using std::cout;
using std::endl;
double total = sum({2.5, 3.1, 4});
cout << "total = " << total << endl;
return 0;
}
double sum(std::initializer_list<double> il)
{
double tot = 0;
for (auto p = il.begin(); p != il.end(); p++)
tot += *p;
return tot;
}
- 头文件
initializer_list
提供了对模板类initializer_list
的支持。这个类包含成员函数begin()
和end()
,用于获悉列表的范围。除用于构造函数外,还可将initializer_list
用作常规函数的参数。
几个小点
- auto
- decltype
- 返回类型后置
C++ 新增了一种函数声明语法:在函数名和参数列表后面指定返回类型:
double f1(double, int); //traditional syntax
auto f2(double, int) -> double;//new syntax,return type is double
- 模板别名
using
C++11提供了另一种创建别名的语法
using itType = std::vector<std::string> ::iterator;
-
空指针
nullptr
-
智能指针(unique_ptr,shared_ptr,weak_ptr)
-
对类的修改
-
右值引用(&&)
右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。
例程
#include <iostream>
inline double f(double tf) {
return 5.0*(tf - 32.0)/9.0;
}
int main()
{
using namespace std;
double tc = 21.5;
double && rd1 = 7.07;
double && rd2 = 1.8 * tc + 32.0;
double && rd3 = f(rd2);
cout << " tc value and address: " << tc << ", " << &tc << endl;
cout << "rd1 value and address: " << rd1 << ", " << &rd1 << endl;
cout << "rd2 value and address: " << rd2 << ", " << &rd2 << endl;
cout << "rd3 value and address: " << rd3 << ", " << &rd3 << endl;
cin.get();
return 0;
}