欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C++ primer 学习笔记(2)

程序员文章站 2022-07-02 15:55:36
八 域和生命期 名字空间域 是不包含在函数声明,函数定义或者类定义内的程序文本部分。 程序员也可以利用名字空间定义namespace definition 来定义用户声明的user-declared...

八 域和生命期

名字空间域

是不包含在函数声明,函数定义或者类定义内的程序文本部分。

程序员也可以利用名字空间定义namespace definition 来定义用户声明的user-declared 的名字空间。它们被嵌套在全局域内。

局部域内的名字解析是这样进行的:

首先查找使用该名字的域, 如果找到一个声明则该名字被解析. 如果没有找到则查找包含该域的域, 这个过程会一直继续下去. 直到找到一个声明或已经查找完整个全局域. 如果后一种情况发生即没有找到该名字的声明, 则这个名字的用法将被标记为错误.

要求全局对象和函数或者只有一个定义或者在一个程序中有多个完全相同的定义, 这样的要求被称为一次定义法则: odr one definition rule

在全局域中定义的对象如果没有指定显式的初始值则该存储区被初始化为0

关键字extern 为声明但不定义一个对象提供了一种方法, 实际上它类似于函数声明, 承诺了该对象会在其他地方被定义或者在此文本文件中的其他地方, 或者在程序的其他文本文件中例如: extern int i;

如果应用程序有很大的头文件, 则使用预编译头文件而不是普通头文件可以大大降低应用程序的编译时间

头文件不应该含有非inline 函数或对象的定义

例如下面的代码表示的正是这样的定义, 因此不应该出现在头文件中

extern int ival = 10;
double fica_rate;

符号常量和inline函数可以被定义多次:
为了使编译器能够用一个常量值替换它的名字, 该常量的定义它的初始值必须在它被使用的文件中可见, 因为这个原因符号常量可以在同一程序的不同文件中被定义多次.

常量指针,指针常量,指向常量的常指针

常量指针

定义为指针指向的变量的值不可通过该指针修改, const在 * 前面
const int*p ( int const * p )

int main(int argc, char *argv[]) {
int a=12;
    int const * p=&a;  // or : const int *p=&a;
    cout<<*p<

指针常量

指针指向的数值可以改变,然而指针所保存的地址却不可以改变。
int* const p

int main(int argc, char *argv[]) {
int a=12,b=13;
    int * const p=&a;    //指针常量
    cout<<*p<

指向常量的常指针

指针指向的地址和数值都是不可更改的。
const int const*p

int main(int argc, char *argv[]) {
int a=12,b=13;
    const  int*  const p = &a;
    cout<<*p<

建议把那些天生无法内联的函数不声明为inline 并且不放在头文件中

内联扩展

是用来消除函数调用时的时间开销。它通常用于频繁执行的函数。 一个小内存空间的函数非常受益。如果没有内联函数,编译器可以决定哪些函数内联。
自动对象的存储分配发生在定义它的函数被调用时.
因为与自动对象相关联的存储区在函数结束时被释放, 所以应该小心使用自动对象的地址, 自动对象的地址不应该被用作函数的返回值. 因为函数一旦结束了该地址就指向一个无效的存储区. 例如:

#include "matrix.h"
matrix* trouble( matrix *pm )
{
{
matrix res;
// 用pm 做一些事情
// 把结果赋值给res
return &res; // 糟糕!
}
int main()
{
matrix m1;
// ...
matrix *mainresult = trouble( &m1 );
// ...
}

< 在本例中该地址可能仍然有效, 因为我们还没有调用其他函数覆盖掉trouble()函数的活动记录的部分或全部, 所以这样的错误很难检测.>

在函数中频繁被使用的自动变量可以用register 声明, 如果可能的话编译器会把该对象装载到机器的寄存器中, 如果不能够的话则对象仍位于内存中. 出现在循环语句中的数组索引和指针是寄存器对象的很好例子.
for ( register int ix = 0; ix < sz; ++ix ) // …
for (register int *p = array ; p < arraysize; ++p ) // …

new 表达式

没有返回实际分配的对象, 而是返回指向该对象的指针。对该对象的全部操作都要通过这个指针间接完成. 例如: int *pi = new int;

空闲存储区的第二个特点是分配的内存是未初始化的. 空闲存储区的内存包含随机的位模式. 它是程序运行前该内存上次被使用留下的结果. 测试: if ( *pi == 0 ) 总是错误的。除非:int *pi = new int( 0 );
如果new 表达式调用的new()操作符不能得到要求的内存通常会抛出一个bad_alloc 异常
内存被释放–> delete pi;
delete 表达式调用库操作符delete(), 把内存还给空闲存储区, 因为空闲存储区是有限的资源. 所以当我们不再需要已分配的内存时就应该马上将其返还给空闲存储区. 这是很重要的.
当程序运行期间遇到delete 表达式时, pi指向的内存就被释放了, 但是指针pi 的内存及其内容并没有受delete 表达式的影响. 在delete表达式之后pi 被称作空悬指针,即指向无效内存的指针, 一个比较好的办法是在指针指向的对象被释放后将该指针设置为0.

野指针

它没有被正确的初始化,于是指向一个随机的内存地址

int main()
{
    int *a=(int *)malloc(sizeof(int));
    *a=1;
    printf("a:%d a'addr:%p\n",*a,a);
    delete(a);
    printf("a:%d a'addr:%p\n",*a,a);     //a dangling pointer 空悬指针,原来所指的内存释放了
    int *b;    //a wild pointer 野指针 ,一开始 就没有赋初值
    printf("b:%d b'addr:%p\n",*b,b);
 return 0;
}
/*
a:1 a'addr:003e2460
a:0 a'addr:003e2460
b:-1869573949 b'addr:7c93154c
*/

auto_ptr 对象被初始化为指向由new 表达式创建的动态分配对象。 当auto_ptr 对象的生命期结束时动态分配的对象被自动释放。 定义pstr_auto 时它知道自己对初始化字符串拥有所有权, 并且有责任删除该字符串。这是所有权授予auto_ptr 对象的责任。相关的头文件:#include
string *pstr_type = new string( “brontosaurus” ); 等价于:auto_ptr< string > pstr_auto( new string( “brontosaurus” ) );
两个指针都持有程序空闲存储区内的字符串地址,我们必须小心地将delete 表达式只应用在一个指针上。即有两个指针指向同一个由new创造的地址,用delete释放一次就可以了
当一个auto_ptr 对象被用另一个auto_ptr 对象初始化或赋值时,左边被赋值或初始化的对象就拥有了空闲存储区内底层对象的所有权,而右边的auto_ptr 对象则撤消所有责任。
auto_ptr< int > p_auto_int; 因为p_auto_int 没有被初始化指向一个对象。所以它的内部指针值被设置为0,这意味着对它解除引用会使程序出现未定义的行为。
操作get()返回auto_ptr 对象内部的底层指针。所以为了判断auto_ptr 对象是否指向一个对象我们可以如下
if ( p_auto_int.get() != 0 &&*p_auto_int != 1024 ) *p_auto_int = 1024;
如果它没有指向一个对象,那么怎样使其指向一个呢——即怎样设置一个auto_ptr 对象的底层指针。我们可以用reset()操作例如
else // ok, 让我们设置 p_auto_int 的底层指针
p_auto_int.reset( new int( 1024 ) );

auto_ptr< string >pstr_auto( new string( “brontosaurus” ) );
// 在重置之前删除对象 brontosaurus:
pstr_auto.reset( new string( “long -neck” ) );
//在这种情况下用字符串操作assign()对原有的字符串对象重新赋值比删除原有的字符率对象并重新分配第二个字符串对象更为有效:
pstr_auto->assign( “long-neck” );
不能用一个指向内存不是通过应用new 表达式分配的指针来初始化或赋值auto_ptr。
release()不仅像get()操作一样返回底层对象的地址而且还释放这对象的所有权:auto_ptr< string > pstr_auto2( pstr_auto.release() );

// 分配单个int 型的对象,用 1024 初始化
int *pi = new int( 1024 );
// 分配一个含有1024 个元素的数组,未被初始化
int *pia = new int[ 1024 ];

动态数组的建立:

对于用new 表达式分配的数组只有第一维可以用运行时刻计算的表达式来指定,其他维必须是在编译时刻已知的常量值。

#include 
#include 

int main(int argc, char *argv[]) {
    puts("please enter height, width, and length:"); 
    int height=10,width=10,length=10;
    scanf("%d%d%d",&height,&width,&length);
    int ***a=new int**[height];
    for(int i=0;i

用来释放数组的delete 表达式形式如下 delete [] str1;
在空闲存储区创建的const 对象有一些特殊的属性:首先const 对象必须被初始化。如果省略了括号中的初始值就会产生编译错误。
const int p; // [error] uninitialized const ‘p’ [-fpermissive]

如果要在局部域中访问全局声明的变量(局部中有一样名字的变量)我们必须使用域操作符:: 例如:

const int m=12;
int cmp(int m){
   m=::m+m;
   return m;
}
int main(int argc, char *argv[]) {
   printf("%d\n",cmp(10));
} 

用户声明的名字空间可以包含嵌套的名字空间.
名字空间的定义可以是不连续的可以跨越多个文件。因此一个名字空间可以在多个文件中被声明

// —– sortlib.c —–
namespace {
void swap( double d1, double *d2 ) { / … */ }
}
函数swap()只在文件sortlib.c 中可见,如果另一个文件也含有一个带有函数swap()定义的未命名名字空间,则该定义引入的是一个不同的函数函数。swap()存在两种定义但这并不是个错误。
因为它们是不同的未命名的名字空间的定义,局部于一个特定的文件不能跨越多个文本文件。

自定义命名空间

跨越多个文件:
a.h:

namespace a{
    void print(){
        printf("here is hero_a\n");  
    } 
} 

b.h:

namespace b{
    void print(){
        printf("here is hero_b\n"); 
    } 
} 

mian.cpp:

#include 
#include 
#include "a.h"
#include "b.h" 
int main(int argc, char *argv[]) {
    a::print(); 
    b::print(); 
} 
/*
here is hero_a
here is hero_b
*/

static:

被static修饰的是静态变量。
静态局部变量保存在全局数据区,而不是保存在栈中

int get(){
    static int g;
    g++;
    return g;
}
int main()
{
    cout<

静态全局变量

未经初始化的静态全局变量会被程序自动初始化为0;
静态全局变量在声明它的整个文件都是可见的,而在文件之外是不可见的(无法多文件传输);

名字空间别名可以用来把一个较短的同义词与一个名字空间名关联起来,例如:namespace international_business_machines
{ /* … */ }
namespace ibm = international_business_machines;
using 声明同其他声明的行为一样它有一个域,它引入的名字从该声明开始直到其所在的域结束都是可见的。
using声明:using cplusplus_primer::matrix;
using指示符:using namespace cplusplus_primer;

九 重载函数

重载

如果两个函数的参数表中参数的个数或类型不同, 则认为这两个函数是重载的.
如果两个函数的返回类型和参数表精确匹配则第二个声明被视为第一个的重复声明.
如果两个函数的参数表相同但是返回类型不同, 则第二个声明被视为第一个的错误重复声明会被标记为编译错误。

当一个参数类型是const 或volatile 时,在识别函数声明是否相同时,并不考虑const 和volatile 修饰符。
1. 声明同一函数
void f( int ); void f( const int ); //参数设为const可以防止参数的值改变。
2. 声明了不同的函数
void f( int* ); void f( const int* ); //const对于指针与引用有影响。

用户不能在using 声明中为一个函数指定参数表: using libs_r_us::print( double );  // 错误
using 指示符使名字空间成员就像在名字空间之外被声明的一样.
指向重载函数的指针:

extern void ff( unsigned int );
void ( *pf1 )( unsigned int ) = &ff;
extern "c" //声明的函数告诉编译器该函数以c语言的方式编译链接。

重载函数集的合适体的选择:
候选函数–可行函数–最佳匹配函。
可行函数的参数个数与调用的实参表中的参数数目相同,或者可行函数的参数个数多一些,但是每个多出来的参数都要有相关的缺省实参。
可能的转换被分成三组:提升promotion, 标准转换standard conversion, 和用户定义的转换user defined conversions。
用户定义的转换由转换函数conversion function 来执行。它是类的一个成员函数允许一个类定义自己的标准转换。
函数转换被划分等级如下:精确匹配比提升好,提升比标准转换好,标准转换比用户定义的转换好。

五种标准转换

1 整值类型转换:从任何整值类型或枚举类型向其他整值类型的转换(不包括前面提升部分中列出的转换)
2 浮点转换:从任何浮点类型到其他浮点类型的转换(不包括前面提升部分中列出的转换)
3 浮点—整值转换:从任何浮点类型到任何整值类型或从任何整值类型到任何浮点类型的转换
4 指针转换:整数值0 到指针类型的转换和任何类型的指针到类型void*的转换
5 bool 转换:从任何整值类型浮点类型枚举类型或指针类型到bool 型的转换
所有的标准转换都被视为是等价的。例如从char 到unsigned char 的转换并不比从char到double 的转换优先级高,类型之间的接近程度不被考虑,即如果有两个可行函数要求对实参进行标准转换以便匹配各自参数的类型则该调用就是二义的。将被标记为编译错误。
函数指针不能用标准转换转换成类型void*。
0 可以被转换成任何指针类型。
实参是该引用的有效初始值(类型一样)则该实参是精确匹配,如果该实参不是引用的有效初始值则不匹配。

十 函数模版 (续读)

这一部分的内容没有读最重要的部分
函数模板提供一个用来自动生成各种类型函数实例的算法。
用预处理器的宏扩展设施,例如#define min(a,b) ((a) < (b) ? (a) : (b)),可以避免为所有的不同类型数据都设计一样算法的函数。
函数模板提供了一种机制,通过它我们可以保留函数定义和函数调用的语义。
在一个程序位置上封装了一段代码,确保在函数调用之前实参只被计算一次而无需像宏方案那样绕过c++的强类型检查。
min函数的模版:

template  //type可以换成任何其他的自定义名字
type min( type a, type b ) {
return a < b ? a : b;
}
template  //模板非类型参数由一个普通的参数声明构成
type min( type (&arr) [size] );

十一 异常处理

throw 表达式可以抛出任何类型的对象, 必须定义可以被抛出的异常, 在c++中异常往往用类class 来实现。
try 块(try block) 必须包围能够抛出异常的语句,try 块以关键字try 开始,后面是花括号括起来的语句序列。在try 块之后是一组处理代码,被称为catch 子句。

当某条语句抛出异常时跟在该语句后面的语句将被跳过程序, 执行权被转交给处理异常的catch子句。
在main()的函数体中声明的变量不能在catch 子句中被引用。
一个catch 子句由三部分构成:关键字catch,在括号中的异常声明exception declaration,以及复合语句中的一组语句。
在查找用来处理被抛出异常的catch 子句时因为异常而退出复合语句和函数定义,这个过程被称作栈展开stack unwinding。
异常对于一个程序非常重要,它表示程序不能够继续正常执行,如果没有找到处理代码程序就调用c++标准库中定义的函数terminate()。terminate()的缺省行为是调用abort(),指示从程序非正常退出。
在异常处理过程中也可能存在单个catch 子句不能完全处理异常的情况,在某些修正动作之后catch 子句可能决定该异常必须由函数调用链中更上级的函数来处理,那么catch子句可以通过重新抛出rethrow 该异常。

catch ( exception eobj ) {
if ( canhandle( eobj ) )
// 处理异常
return;
else
// 重新抛出它, 并由另一个catch 子句来处理
throw;
}
void calculate( int op ) {
try {
// 被 mathfunc() 抛出的异常的值为 zeroop
mathfunc( op );
}
//为了修改原来的异常对象catch 子句中的异常声明必须被声明为引用例如
cacth ( ehstate &eobj ) {
// 修改异常对象
eobj = severeerr;
// 被重新抛出的异常的值是 severeerr
throw;
}

十二 泛型算法

泛型算法

操作在多种容器类型上的算法。
比如find()函数,iterator迭代器,sort()函数。
————————————————————————————————————————
int数组的find()算法:

#include 
#include 
using namespace std;

int main()
{
    int search_value;
    int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
    cout << "enter search value: ";
    cin >> search_value;
    int *presult = find( &ia[0], &ia[6], search_value );
    cout << "the value " << search_value<< ( presult == &ia[6]? " is not present" : " is present" )<< endl;
    return 0;
}

————————————————————————————————————————-
vector的find()算法:

#include 
#include 
#include 
using namespace std;

int main()
{
    int search_value;
    int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
    vector vec( ia, ia+6 );
    cout << "enter search value: ";
    cin >> search_value;
    vector::iterator presult;
    presult = find( vec.begin(), vec.end(), search_value );
    cout << "the value " << search_value<< ( presult == vec.end()? " is not present" : " is present" )
    << endl;
    return 0;
}

——————————————————————————————————————————-
list的find()算法

#include 
#include 
#include 
using namespace std;

int main()
{
    int search_value;
    int ia[ 6 ] = { 27, 210, 12, 47, 109, 83 };
    list ilist( ia, ia+6 );
    cout << "enter search value: ";
    cin >> search_value;
    list::iterator presult;
    presult = find( ilist.begin(), ilist.end(), search_value );
    cout << "the value " << search_value
    << ( presult == ilist.end()
    ? " is not present" : " is present" )
    << endl;
    return 0;
}

对容器的遍历:iterator. 指针的泛化。
预定义函数对象被分成算术关系和逻辑操作, 每个对象都是一个类模板, 其中操作数的类型被参数化. 为了使用它们我们必须包含#include
以降序排列容器:
vector< string > svec;
// …
sort( svec.begin(), svec.end(), greater()); // 预定义的类模板greater 它调用底层元素类型的大于操作符

const 容器只能被绑定在const iterator 上这样的要求与const 指针只能指向const 数组的行为一样在两种情况下c++语言都努力保证const 容器的内容不会被改变

为支持泛型算法全集根据它们提供的操作集标准库定义了

五种iterator

inputiterator, outputiterator, forwarditerator, bldirectionaliterator 和randomaccessiterator
inputiterator 可以被用来读取容器中的元素, 但是不保证支持向容器的写入操作
outputiterator 可以被用来向容器写入元素, 但是不保证支持读取容器的内容
forwarditerator 可以被用来以某一个遍历方向向容器读或写
bidirectionaliterator 从两个方向读或写一个容器
randomaccessiterator 除了支持bidirectionaliterator 所有的功能之外还提供了在常数时间内访问容器的任意位置的支持

元素范围概念有时称为左闭合区间通常写为[ first, last ) // 读作: 包含 first 以及到 但不包含 last 的所有元素

四个算术算法

#include
adjacent_difference()、 accumulate()、 inner_product()和partial_sum()

查找算法

equal_range()、lower_bound()和upper_bound() (二分查找实现)

int main()
{
    int a[6]={4,10,13,14,19,26};
    int *val=lower_bound(a,a+6,12);  //first value  >= goal
    cout<<*val< goal
    cout<<*val<

函数equal_range()返回first和last之间等于val的元素区间.返回值是一对迭代器。

排列组合算法

next_permutation(), prev_permutation()

int a[3]={1,2,3};
void show(){
    for(int i=0;i<3;i++){
        printf("%d ",a[i]);
    }
    cout<