C++学习-3-第一篇-20221223

这篇博客详细介绍了C++的基础元素,包括内置数据类型、标准库、函数定义和使用。强调了C++程序的基本结构,展示了文件输入输出的实现,并探讨了类的构造、析构、赋值和拷贝操作。此外,还讨论了数组和指针的特性,以及类模板、异常处理和名字空间的使用。博客还涉及到了动态内存管理和对象生命周期,以及类型安全和编程最佳实践。
摘要由CSDN通过智能技术生成

封面

第一篇 概述

ch1 C++语言的基本元素

(内置数据类型 对象的定义 表达式 语句函数的定义和使用)

1.标准 C++头文件没有后缀。

2.C++标准库还提供了一组扩展的基本数据类型 其中包括字符串 string 、复数 complex number、 向量 vector 和列表 list 。

3.c++程序基本结构实例函数:

#include <iostream> 
using namespace std; 
void read() { cout << "read()\n"; } 
void sort() { cout << "sort()\n"; } 
void compact() { cout << "compact()\n"; } 
void write() { cout << "write()\n"; } 
int main() 
{ 
 read(); 
 sort(); 
 compact(); 
 write(); 
 return 0; 
}

#ifdef 指示符常被用来判断一个预处理器常量是否已被定义 以便有条件地包含程序代
码 例如
#ifdef DEBUG
cout << “Beginning execution of main()\n”;
#endif

5.文件输出

#include <iostream> 
#include <fstream> 
#include <string> 
int main() 
{ 
 ofstream outfile( "out_file" ); 
 ifstream infile( "in_file" ); 
 if ( ! infile ) { 
 cerr << "error: unable to open input file!\n"; 
 return -1; 
 } 
 if ( ! outfile ) { 
 cerr << "error: unable to open output file!\n"; 
 return -2; 
 } 
 string word; 
 while ( infile >> word ) 
 outfile << word << ' '; 
 
 return 0; 
}

ch2 C++浏览

练习 2.1 为什么内置数组类型不支持数组之间的赋值? 支持这种操作需要什么信息?
Answer:数组名其实代表着一个指针常量,所以将一个数组名赋值给另一个数组,就像是把常量2赋值给常量3一样,虽然语法正确,但在语意层面上会产生错误。C++语言并未支持数组的赋值操作,编译器在编译时必须知道数组的长度,才能产生执行代码来支持数组对数组的操作。*

练习 2.2 你认为作为一等公民的数组应该支持什么操作?
取得数组长度; .互相拷贝; .做相等比较;// 不理解这个问题one-class

动态对象的分配与释放 必须由程序员显式地管理 相对来说比较容易出错
它通过 new 和 delete 两个表达式来完成

练习 2.3 说出下面定义的四个对象之间的区别 (a) int ival = 1024; ( c) int *pi2 = new int(
1024 ); (b) int *pi = & ival; (d) int *pi3 = new int[ 1024 ];

answer:静态,分配了一个没有名字的 int 类型的对象 对象初始值为 1024,指针指向ival,指针指向数组

练习 2.4 下面的代码段是做什么的? 有什么严重错误? 注意 指针 pia 的下标操作符的用法是正确的 在 3.9.2
节中我们会解释其理由 int *pi = new int( 10 ); int *pia = new int[ 10 ]; while
( *pi < 10 ) { pia[ *pi ] = *pi; *pi = *pi + 1; } delete pi;
delete [] pia;

answer:转载:
https://blog.csdn.net/yqzsl/article/details/5581744

我来解释一下每行的代码语义: //动态分配一个int大小的内存空间,其内容初始化为10将其地址存放到pi中 int *pi = new
int( 10 );

/动态分配一个能容纳10个int的内存空间,未作初始化操作。 int *pia = new int[ 10 ];

//当*pi(也就是10)小于10的时候,执行循环语句 while ( *pi < 10 ) { //pia[ 10 ]
赋值为10将其开始地址存放到pia中

pia[ *pi ] = *pi;

//*pi的内容累加1
*pi = *pi + 1; }

//释放pi指向的整数内存 delete pi;

//释放pia指向的整数数组内存 delete [] pia;

问题: 1)循环语句条件判断为False,所以循环体中的语句根本不执行。(理解) 2)
pia[]的index没有经过边界检测,在上例中,pia[]从0~9一共10个int元素,但是*pi的值为10,明显是数组越界访问。()
3)pia[]中的内容未作初始化处理,里面包含垃圾数据。

// 相等与不相等操作 #2b 
 bool operator==( const IntArray& ) const; 
 bool operator!=( const IntArray& ) const; 
 // 赋值操作符 #2a 
 IntArray& operator=( const IntArray& );


????不理解这里

两种操作符
用于类对象的点操作符 .
用于类对象指针的箭头操作符 ->

下面 给出两个 IntArray 对象
IntArray myArray0, myArray1; 
 赋值操作符可以这样应用
// 调用拷贝赋值成员函数
// myArraya.operator=( myArray1 ) 
myArray0 = myArray1; 
 等于操作符的调用如下所示
// 调用等于成员函数
// myArray0.operator==( myArray1 ) 
if ( myArray0 == myArray1 ) 
 cout << "!!our assignment operator works!\n";


class IntArray { 
public: 
 // ... 
 int size() const { return _size; } 
private: 
 // 内部数据
 int _size; 
 int *ia; 
}; 

 由于我们把_size 放在类的私有区内 因此我们有必要声明一个公有成员函数 以便允
许用户访问它的值 由于 C++不允许成员函数与数据成员共享同一个名字 所以在这样的情
况下 一般的习惯是在数据成员名字前面加一个下划线 _ 因此 我们有了公有访问函数
size()和私有数据成员_size 在本书以前的版本中 我们在访问函数前加上 get 或 set 实践证
明这样做有些累赘
IntArray:: 
IntArray( const IntArray &rhs ) 
{ 
 // 拷贝构造函数
 _size = rhs._size; 
 ia = new int[ _size ]; 
 for (int ix = 0; ix < _size; ix++ ) 
 iz[ ix ] = rhs.ia[ ix ]; 
}
提到的概念:
类体的外面定义类的成员函数
双冒号 :: 操作符被称为域操作符 scope operator
拷贝构造函数 copy constructor
一种新的复合类型 :引用 reference
析构成员函数 destructor member function  标识用: ~

练习 2.5
C++类的关键特征是接口与实现的分离 接口是一些 用户可以应用到类对象上的操作
的集合 它由三部分构成 这些操作的名字 它们的返回值 以及它们的参数表 一般地 这
些就是该类用户所需要知道的全部内容 私有实现包括为支持公有接口所必需的算法和数据
理想情况下 即使类的接口增长了 它也不用变得与以前的版本不相兼容 另一方面 在类的
生命周期内其实现可以自由演化 从下面选择一个抽象 指类 并为该类编写一个公共接口
(a) Matrix © Person (e) Pointer
(b) Boolean (d) Date (f) Point

练习 2.6
构造函数和析构函数是程序员提供的函数 它们既不构造也不销毁类的对象 编译器自
动把它们作用到这些对象上 因此构造函数 constructor 和析构函数 destructor 这两个词
多少有些误导 当我们写
int main() {
IntArray myArray( 1024 );
// …
return 0;
}
在构造函数被应用之前 用于维护 myArray 中数据成员的内存已经被分配了 实际上
编译器在内部把程序转换成如下的代码 注意这不是合法的 C++代码5
int main() {
IntArray myArray;
// 伪 C++代码–应用构造函数
myArray.IntArray::IntArray( 1024 );
// …
// 伪 C++代码–应用析构函数
myArray.IntArray::~IntArray();
return 0;
}
类的构造函数主要用来初始化类对象的数据成员 析构函数主要负责释放类对象在生命
期内申请到的所有资源 请定义在练习 2.5 中选择的类所需要的构造函数集 你的类需要析
构函数吗

练习 2.7
在练习 2.5 和练习 2.6 中 你差不多已经定义了使用该类的完整公有接口 我们还需
要定义一个拷贝赋值操作符 但是现在我们忽略这个事实——C++为 从一个类对象向另一
个类对象赋值 提供了缺省支持 问题在于 缺省的行为常常是不够的 这将在 14.6 节中讨
论 写一个程序来实践在前面两个练习中定义的公有接口 用起来容易还是麻烦 你希望
重写这些定义吗 你能在重写的同时保持兼容性吗

class IntArray { 
public: 
 // 构造函数
 explicit IntArray( int size = DefaultArraySize ); 
 IntArray( int *array, int array_size ); 
 IntArray( const IntArray &rhs ); 
 
 // 虚拟析构函数
 virtual ~IntArray() { delete [] ia; } 
 
 // 等于和不等于操作
 bool operator==( const IntArray& ) const; 
 bool operator!=( const IntArray& ) const; 
 IntArray& operator=( const IntArray& ); 
 int size() const { return _size; } 
 
 // 去掉了索引检查功能 . . . 
 virtual int& operator[](int index) { return ia[index]; } 
 virtual void sort(); 
 virtual int min() const; 
 virtual int max() const; 
 virtual int find( int value ) const; 
protected: 
 // 参见 13.5 节的说明
 static const int DefaultArraySize = 12; 
 void init( int sz, int *array ); 
 int _size; 
 int *ia; 
};

类型相关的函数标记为虚函数,它的算法由特定的基类或派生类的行为或实现来决定
#ifndef IntArrayRC_H 
#define IntArrayRC_H 
#include "IntArray.h" 
class IntArrayRC : public IntArray { 
public: 
 IntArrayRC( int sz = DefaultArraySize ); 
 IntArrayRC( int *array, int array_size ); 
 IntArrayRC( const IntArrayRC &rhs ); 
 virtual int& operator[]( int ); 
 
private: 
 void check_range( int ); 
}; 
#endif 

派生类对象的初始化过程是这样的 首先自动调用每个基类的构造函数来初始化相关的基类子对象 然后再执行派生类的构造函数 从设计的角度来看派生类的构造函数应该只初始化那些在派生类中被定义的数据成员 而不是某类中的数据成员。

这个小程序实现了 IntArray 与 IntArrayRC 两个类的层次结构


#include <iostream> 
#include <IntArray.h> 
#include <IntArrayRC.h> 
extern void swap(IntArray&,int,int); 
int main() 
{ 
 int array[ 4 ] = { 0, 1, 2, 3 }; 
 IntArray ia1( array, 4 ); 
 IntArrayRC ia2( array, 4 ); 
 
 // 错误 一位偏移 off-by-one 应该是 size-1 
 // IntArray 对象捕捉不到这个错误
 cout << "swap() with IntArray ia1\n"; 
 swap( ia1, 1, ia1.size() ); 
 
 // ok: IntArrayRC 对象可以捕捉到这样的错误
 cout << "swap() with IntArrayRC ia2\n"; 
 swap( ia2, 1, ia2.size() ); 
 return 0; 
} 
 
编译并执行这个程序 产生如下结果
swap() with IntArray ia1 
swap() with IntArrayRC ia2

练习 2.8
一般来说 类型/子类型继承关系反映了一种 is-a 是一种 的关系 具有范围检查
功能的 ArrayRC 是一种 Array 一本书 Book 是一种图书外借资源 LibraryRentalMaterial
有声书 AudioBook 是一种书 Book 等等 下面哪些反映出这种 is-a 关系
(a)成员函数是一种(isA_kindOf)函数
(b)成员函数是一种类
©构造函数是一种成员函数
(d)飞机是一种交通工具
(e)摩托车是一种卡车
(f)圆形是一种几何图形
(g)正方形是一种矩形
(h)汽车是一种飞机
(i)借阅者是一种图书馆

练习 2.9
判断以下操作哪些可能是类型相关的 因此可把它们定义为虚拟函数 哪些可以在所有
类之间共享 对单个基类或派生类来说哪些是惟一的
(a) rotate(); (b) print();
© size(); (d) dateBorrowed();
(e) rewind(); (f) borrower();
(g) is_late(); (h) is_on_loan();

练习 2.10
对于保护 protected 访问级别的使用已经有了一些争论 有人认为 使用保护访问级别允许派生类直接访问基类的成员 这破坏了封装的概念 因此 所有的基类实现细节都应该是私有的 private 另外一些人认为 如果派生类不能直接访问基类的成员 那么派生类的实现将无法有足够的效率供用户使用如果没有关键字 protected 类的设计者将被迫把基类成员设置为 public 你怎样认为

练习 2.11
第二个争论是关于将成员函数显式地声明为 virtual 的必要性 一些人认为 这意味着如
果类的设计者没有意识到一个函数需要被声明为 virtual 则派生类的设计者就没有办法改写
这个关键函数 因此 他们建议把所有成员函数都设置为 virtual 的 另一方面 虚拟函数比
非虚拟函数的效率要低一些 6
因为它们不能被内联 内联发生在编译时刻 而虚拟函数是
在运行时刻被处理的 所以它们可能是运行时刻效率低下的原因之一 尤其是小巧而又被
频繁调用的 与类型无关的函数 比如 Array 数组 的 size 函数 你又怎样认为呢

练习 2.12
下面的每个抽象类型都隐式地包含一族抽象子类型 例如 图书馆藏资料
LibraryRentalMaterial 抽象隐式地包含书 Book 音像 Puppet 视盘 Video 等
选择其中一个 找出该抽象的子类型层次 并为这个层次指定一个小的公有接口 且其中包
括构造函数 如果存在的话 指出哪些函数是虚拟的 并且写一小段伪代码程序来练习使用
这个公有接口
(a) Points (b) Employees
( c) Shapes (d) TelephoneNumbers
(e) BankAccounts (f) CourseOfferings

下面的例子演示了怎样使用 Array 类模板
#include <iostream> 
#include "Array.h" 
int main() 
{ 
 const int array_size = 4; 
 
 // elemType 变成了 int 
 Array<int> ia(array_size); 
 
 // elemType 变成了 double 
 Array<double> da(array_size); 
 
 // elemType 变成了 char 
 Array<char> ca(array_size); 
 int ix; 
 
 for ( ix = 0; ix < array_size; ++ix ) { 
 ia[ix] = ix; 
 da[ix] = ix * 1.75; 
 ca[ix] = ix + 'a'; 
 } 
 
 for ( ix = 0; ix < array_size; ++ix ) 
 cout << "[ " << ix << " ] ia: " << ia[ix] 
 << "\tca: " << ca[ix] 
 << "\tda: " << da[ix] << endl; 
 return 0; 
}

练习 2.13
给出下列类型声明
template class Array;
enum Status { … };
typedef string Pstring;
如果存在的话 下面哪些对象的定义是错误的
(a) Array< int
& > pri( 1024 );
(b) Array< Array > aai( 1024 );
© Array< complex< double > > acd( 1024 );
(d) Array< Status > as( 1024 );
(e) Array< Pstring > aps( 1024 );

练习 2.14
重写下面的类定义 使它成为一个类模板
class example1 {
public:
example1( double min, double max );
example1( const double *array, int size );

double& operator[]( int index ); bool operator==( const example1& )
const; bool insert( const double*, int ); bool insert( double );
double min() const { return _min; }; double max() const { return
_max; }; void min( double ); void max( double ); int count( double value ) const; private: int size; double *parray;
double _min; double _max; };

练习 2.15 给出如下的类模板 template class Example2 { public:
explicit Example2( elemType val = 0 ) : _val( val ){} bool min(
elemType value ) { return _val < value; } void value( elemType
new_val ) { _val = new_val; } void print( ostream &os ) { os <<
_val; } private: elemType _val; }; template ostream& operator<< ( ostream &os, const Example2 &ex ) {
ex.print( os ); return os; } 如下这样写会发生什么事情 (a) Example2< Array* >
ex1; (b) ex1.min( &ex1 ); © Example2< int > sa( 1024 ), sb; (d)
sa = sb; (e) Example2< string > exs( “Walden” ); (f) cout << "exs: "
<< exs << endl;

练习 2.16 在 Example2 的定义中 我们写 explicit Example2( elemType val = 0 ) : _val( val ){} 其意图是指定一个缺省值 以便用户可以写 Example2< Type > ex1( value ); Example2< Type > ex2; 但是 我们的实现把 Type 限制在一个 不能用 0 进行初始化的类型 的子集中 例如 用 0 初始化一个 string 类型 就是一个错误 7 类似的情况是 如果 Type 不支持输出操作 符 那么 print()调用就会失败 因此 Example2 的输出操作符也会失败 如果 Type 不支 持小于操作符 那么 min()调用就会失败 ,C++语言本身并没有提供可以指示在实例化模板时 Type 有哪些隐含限制的方法 在最坏 的情况下 当程序编译失败时程序员才发现这些限制,你认为 C++语言应该支持限制 Type 的语法吗 如果你认为应该的话 请说明语法 并用它重写 Example2 的定义 如果认为不 需要
请说明理由

练习 2.17
在上一个练习中 我们说如果 Type 不支持输出操作符和小于操作符 那么对 print()和
min()的调用就会出错 在标准 C++中 错误的产生不是发生在类模板被创建的时候 而是在
print()与 min()被调用的时候 你认为这样的语义正确吗 是否应该在模板定义中标记这个错
误 为什么

在 C++中 异常的抛出由 throw 表达式来执行 例如 在下面的程序段中 一个 string 类型的异常被抛出来以便响应打开文件失败异常.C++中, 异常的处理由 catch 子句来执行,catch 子句与 try 块相关联 一个 try 块用一个或多个 catch 子句将一条或多条语句组织起来 .

if ( ! infile ) { 
 string errMsg( "unable to open file: " ); 
 errMsg += fileName; 
 throw errMsg; 
}

练习 2.18
下面的函数对可能的非法数据以及可能的操作失败完全没有提供检查 找出程序中所有可能出错的地方 本练习中, 我们不关心可能会抛出的异常。
int *alloc_and_init( string file_name )
{
ifstream infile( file_name );
int elem_cnt;
infile >> elem_cnt;
int *pi = allocate_array( elem_cnt );
int elem;
int index = 0;
while ( cin >> elem )
pi[ index++ ] = elem;
sort_array( pi, elem_cnt );
register_data( pi );
return pi;
}

练习 2.19
alloc_and_init()函数会调用到下面的函数 如果这些函数调用失败了 它们将抛出相应类型的异常
allocate_array() noMem
sort_array() int
register_data() string
请在合适的地方插入一个或多个 try 块以及相应的 catch 子句来处理这些异常 在 catch 子句中只需简单地输出错误的出现情况

练习 2.20
检查在练习 2.18 中的函数 alloc_and_init()中所有可能出现的错误 指出哪些错误会抛出异常 修改该函数 或用练习 2.18 的版本 或用练习 2.19 的版本 来抛出对被识别的异常抛出文字串就可以了

练习 2.21
给出如下名字空间定义
namespace Exercise {
template
class Array { … };
template
void print( Array< Etype > );
class String { … };
template
class List { … };
}
以及下面的程序
int main() {
const int size = 1024;
Array< String > as( size )
54 第二章 C++浏览
List< int > il( size );
// …
Array< String > *pas = new Array(as);
List *pil = new List(il);
print( *pas );
}
同为类型名被封装在名字空间中 所以当前程序编译失败 把程序修改为
1 用限定名字修饰符来访问名字空间 Exercise 中的类型定义
2 使用 using 声明来访问类型定义
3 用名字空间别名机制
4 用 using 指示符

练习 2.22
解释每个 vector 定义的结果
string pals[] = { “pooh”, “tigger”, “piglet”, “eeyore”,“kanga”};
(a) vector svec1( pals, pals+5);
(b) vector ivec1( 10 );
© vector ivec2( 10, 10 );
(d) vector svec2( svec1 );
(e) vector dvec;

练习 2.23
已知下列函数声明 请实现 min()的函数体 它查找并返回 vec 的最小元素 要求首先使用 索引 vec 中元素的 for 循环 来实现 min() 然后 再使用 通过迭代器遍历 vec 的 for 循环 来实现 min() template
template
elemType
min( const vector &vec );

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值