管理内存和低级数据结构————第十章心得


理解标准库的关键在于使用核心语言编程工具于技巧(“低级(low level)”)。它们比标准库更加底层,于一般计算硬件的工作方式更加接近。因此比标准库更加难以使用,更加危险。

由于标准库无法解决全部问题,因此C++程序中经常使用“低级”技巧。

1 指针

C++的变量实际就是一段字节,例如char是1个字节,32位int是4个字节,64位long long是8个字节。为了可以正确访问这些字节,每段变量字节都有唯一的编号。我们把内存中字节的编号称为地址或指针。而存放指针的变量称为指针变量,例如int*

例如

int x = 10;
// 假设x的地址为0x7ff7bdf8b888
int* p = &x;
//那么p存放的值就是0x7ff7bdf8b888
*p;//就是取p值中的值上(即,地址上)的实际存放的值10

1.1 概念

指针存放的是对象地址的值。

假设x是一个对象,那么&x就是该对象的地址;
如果p是一个对象的地址,那么*p就是该对象本身。
其中&是求地址运算符,*p中*是间接引用运算符。

可以将一个对象理解为一个只包含该对象一个元素的“容器”,而将指向该对象的指针理解成一个指向一个“容器”中唯一的元素的迭代器。

1.2 空指针

指针类型的局部变量在被赋值之前没有任何有意义的值。

通常用0来初始话指针变量,这是由于将0转换成指针值可以确保产生一个与指向具体对象的指针不同的值。

常量0也是唯一可以用于转换成指针类型的整型值。将0转换成的指针类型值为空指针。

1.3 使用

定义一个指针变量

//定义一个int型指针变量
int *p;
int* p;//C++程序员习惯用法

容易出错点:
下面写法中,实际上定义了一个int*类型的指针p,以及定义一个int类型的q变量。

int* p, q;

示例程序:

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

int main(int argc, char** argv){
    int x = 5;
    int* p = &x;//指向x地址的指针
    cout << "x = " << x << endl;
    *p = 6;
    cout << "x = " << x <<endl;  
    return 0;
}

上面的程序中,一旦p存储了x的地址,*p和x将是指向同一对象的两种完全等效的方法。

1.4 指向函数的指针

1.4.1 使用

下面的代码中,*p具有int类型,p是一个指针。

int *p;

下面的代码中,我们间接引用了fp,调用它是以一个int型变量作为参数,返回结果也具有int型。也就是说,fp是一个指向具有一个int类型参数并返回int类型结果的函数的指针。

int (*fp)(int);

1.4.2 原理

函数不是对象,无法对其进行复制或者赋值,也无法将它们直接作为参数。特别是在程序中无法创建或者修改函数(只有编译器可以)。

一个程序对函数进行的全部操作只有调用该函数或者得到它的地址

如果在任何地方出现一个函数名而不是调用该函数时,即使没有显示使用&声明,编译器都会将它解释为该函数的地址。

如我们有一个与fp函数类型匹配的函数

int next(int n){
    return n + 1;
}

那么下面任何一种写法都是等价的

int (*fp)(int);

fp = &next;//与下面等价
fp = next;

如果有一个int型变量i,可以通过fp函数调用next来让i加1,下面两种实现方法等价:

int i;

i = (*fp)(i);//与下面等价
i = fp(i);

完整示例程序:

#include <iostream>

using std::cout;
using std::cin;
using std::endl;

int next(int n){
    return n + 1;
}

int main(int argc, char** argv){
    int (*fp)(int);
    
    fp = &next;//将fp指向next函数
    //fp = next;

    int i = (*fp)(6);
   // int i = fp(6);

    cout << i <<endl;
    return 0;
}

如果编写一函数,表面上这个函数以另一函数为参数,编译器会在背后悄悄将该参数转换为一个指向函数的指针。

例如:
下面两种写法是等价的:

void write(ostream& , int test(int ), int )
void write2(ostream& , int (*test)(int ), int )

完整示例程序:

//返回指向函数的指针
#include <iostream>
#include <ostream>

using std::cout;
using std::cin;
using std::endl;
using  std::ostream;

int test1(int a){
    return  a + 1;
}

int test2(int b){
    return  b + 3;
}

void write(ostream& out, int test(int ), int a){
    out << test(a) << endl;
}

void write2(ostream& out, int (*test)(int ), int a){
    out << test(a) << endl;
}

int main(int argc, char** argv){
    int x = 10;

    write(cout, test1, x);
    write(cout, test2, x);

    write2(cout, test1, x);
    write2(cout, test2, x);
    return 0;
}

但是这一转变对于函数的返回值却不会自动执行。如果我们想写一个返回类型为指向函数的指针的函数,其返回类型要求和与write2函数的类型相同,就必须显示声明返回一个指针。

下面两种显示方法相同:

typedef int (*test_fp)(int );
fp test();

等价于

int (*test())(int );

作为结果调用test()函数并且间接引用结果,那么将会得到一个返回类型为int,以int类型作为参数的函数

两种声明方式,返回一个指向带有int引用型变量参数的函数的指针:

//声明
typedef int (*test_fp)(int& );
test_fp test3(int&);

等价于下面的

//声明
int (*test4(int& ))(int& );

完整代码:

//返回指向函数的指针
#include <iostream>
#include <ostream>

using std::cout;
using std::cin;
using std::endl;
using  std::ostream;

int test1(int& a){
    a++;
    return a;
}

//声明
typedef int (*test_fp)(int& );
test_fp test3(int&);
//定义
test_fp test3(int& x){
    int (*fp)(int& ) = &test1;
    (*fp)(x);//x的值变为11
    return fp;
}

//声明
int (*test4(int& ))(int& );
//定义
int (*test4(int& x))(int& ){
    int (*fp)(int& ) = &test1;
    (*fp)(x);//x的值变为11
    return fp;
}


void write2(ostream& out, int test(int& ), int a){
    out << test(a) << endl;
}

int main(int argc, char** argv){
    int x  = 10 ;
    //test传递的时引用会导致x变化,write传递的没哟引用,计算出值后会被丢弃
    //在10的基础上加2
    //第一调用test时加1,由于是引用,x会实际变为11,第二次调用write时加1,虽然结果是12,但是x值还是11
    write2(cout, test3(x), x);
    write2(cout, test4(x), x);//在11的基础上加2
    return 0;
}

库算法中指向函数的指针常被用于另一个另一函数的参数

template<class In, class Pred>
In find_if(In begin, In end, Pred f){
	while(begin ! = end && !f(*begin)){
		++begin;
	}
	return begin;
}

当f(*begin)具有一个意义的值时,Pred可以时任何类型。先假设一个判断函数,定义如下:

bool is_negative(int n){
	return n < 0;
}

我们用find_if找向量v中第一个负值的元素

vector<int>::iterator i = find_if(v.begin(), v.end(), is_negative);

我们可以将&is_negative写成is_negative,编译器会自动将函数名转化成指向函数的指针。

在find_if函数的实现代码中也可以将(*f)(*begin)写成f(begin),编译器会对函数指针的调用自动解释为调用该指针指向的函数。

1.5 指针算法

指针是一个随机存储的迭代器,因此有

随机存储的迭代器的必要条件(p和q是迭代器,n是一个整数):

  • p + n, p - n , n + p
  • p - q

两个迭代器(p - q)相减会产生一个整数,表示p 和q指向的元素在容器中的间距。由于p-q可能是负值,因此它是一个带符号的整数类型。该类型到底是整型(int)还是长整型(long)取决于系统环境。标准库中在<cstddef>中提供了ptrdiff_t来表示这样的类型。

除了指针的加法外,指针可以做减法,指针可以和整数做加、减法。

对于数组
double a[3];

a + 3是一个有效指针,尽管它不指向数组a中的任何一个元素。类似于string和vector这两个容器,对于一个只有n个元素的数组的首地址加n得到一个新地址,该地址不指向数组中的任何对象,但它是有效的。

对于与指针p,整数n相关的p,p+n,p-n,即使它们有些可能会超过数组的地址范围,都是有效的,只是不可预测而已。

对指向一个容器前面的地址的迭代器进行计算是不允许的。类似的对于数组前面的地址进行计算被视为是非法的。

2 数组

数组是容器的一种,与向量(vector)相似,但没有向量强大的功能(不能动态增加或减小尺寸)。

指针和数组都是C/C++最原始的数据结构之一,只使用数组,不使用指针不可能解决任何问题。指针的强大功能在使用数组时,可以得到更好的体现。

数组并不是类,因此没有成员函数和成员变量。

2.1 数组长度类型

经常使用<cstddef>头文件中的size_t类型(无符号整型)表示数组的大小,size_t类型的大小可以装在任何对象。

2.2 数组名

只要我们将数组名作为一个值使用,数组名都将表示指向数组首地址的指针。也就是数组吗保存了首地址。

可以使用*运算符来对数组间接引用,以访问该指针指向的对象。

例如:

double a[3];
*a = 6.6;//将6.6赋给a数组的首元素

2.3 索引

与全部随机访问迭代器相同,它们(指针)都支持索引功能。

如果p指向一个数组中的第m个元素,那么p[n]就代表数组中的第n+m个元素本身。(并不是代表该元素的地址,而是代表该元素本身)。

如果a是一个数组,那么a[n]就是数组的第n个元素。或者说,如果p是一个指针,而n是一个整数,那么p[n]和*(p+n)等价。

2.4 初始化

我们可以避免显示的定义数组的大小,

int a[] = {1,2,3,4,5,6};

数组将1作为第0号,6作为第5号。

编译器会自动计算数组的个数。

3 字符串字面量

字符串字面量:一个字符字面量数组,该数组的大小是字符串的长度加1,多出来的一个字符是编译器自动在其他字符后面加上的一个空字符(’\0’)。

也就是说

const char love[] = {'L', 'o', 'v', 'e', '\0'};

love数组和字符串字面量“Love”等价。

注:讲一个字符数组定义为静态变量,会自动在数组末尾加上’\0’。

 static char love[] = {'L', 'o','v','e'};//初始化化时,自动末尾加'\0'

strlen函数可以求一个字符串变量的大小或一个以空字符结尾的数组的大小。strlen函数返回的大小没有算上空字符这个终止符。

strlen的库函数实现:

size_t strlen(const char* p){
	size_t size = 0;
	while(*p++ 1= '\0'){
		++size;
	}
	return size;
}

其中size_t是无符号整型类型,它足够大,可以容纳任何数组。

由于love数组和字符串字面量“Love”等价,因此有

string s(love);

string s2("Love");

string s3(love, love + strlen(love));

上面三种等价。

3.1 初始化字符串指针数组

static const double numbers[] = {
      97, 94, 90, 87, 84, 80, 77, 74, 70, 60, 0
    };
 static const size_t ngrades = sizeof(numbers)/ sizeof(*numbers);//计算该元素的个数
 //整个数组的大小/每个元素占用的空间大小

上面的程序中,使用static是为了告诉编译器使用numbers数组之前只要进行一次初始化。如果不使用static,编译器会在每次调用数组之前进行初始化。很明显,这回减慢程序的速度。

sizeof运算符返回的数值以字节(bytes)为单位,这是实际存储的单位,因具体编程工具的不同而有差异。

字节唯一可以肯定的是一个字节包含8位(bit),每个对象至少占用一个字节,一个字符(char)变量正好占用一个字节的空间。

注意点:

return "? \? \?";//C++不允许存在连续两个或多个问号

4 main函数的参数

main函数的参数有两个,一个整型(int)参数(argc)于一个指向字符指针的指针参数(argv)。

argv是一个指向指针数组首元素地址的指针。数组中每个元素都指向一个字符串参数。

argc是argv指向的数组中的指针个数。

argv数组的首元素总是main函数编译后的程序名,因此argc的值至少是1,如果有参数,那些参数总是在数组中作为连续的几个元素出现。

例如下面的程序中,在命令端执行.cpp文件并加上参数Hello, word,程序会输出
在这里插入图片描述
其中第一个参数为文件名。

//mian函数的参数
#include <iostream>

using std::cout;
using std::cin;
using std::endl;

int main(int argc, char** argv){
    if(argc > 1){
        cout << argv[0];
        for (int i = 1; i != argc; ++i) {
            cout <<" " << argv[i];
        }
    }
    cout << endl;
    cout << argc <<endl;
    return 0;
}

一般char* argv[]和char**是等价的,但是char* argv[]这种语法只有在参数列表中出现时才是合法的。

5 文件读写

5.1 标准错误流

如果程序出现非正常情况,我们希望有一种方式可以将异常输出,这一输出将告诉用户程序出现了程序,或者出现异常后能建立一个事件日志。

clog流倾向于生成日志,因此clog流和cout有着一样的缓冲特性;平时存储错误信息,在系统认为适当时将它们输出。

cerr流则是即使输出错误信息,这可以保证一发生异常,就会及时显示错误信息。

5.2 输入/输出文件

文件输入/输出的对象和用于流的输入/输出对象具有不一样的类型。

在标准库总,ifstream类型对象被定义istream的一种,ofstream是ostream的一种。因此,可以在任何需要istream的地方使用ifstream,在任何需要ostream的地方使用ofstream。

在定义一个ifstream或ofstream类型对象时,会要求提供指向空字符结尾的字符数组的首元素的指针,例如string类型形式的文件名。

原因有:

  • 1 这种方式可以使程序用到输入/输出库的特性而不依赖于string类型的特性;
  • 2 输出/输出库的出现比string类要早好几年;
  • 3 以这种方式提供文件名的时候,可以使程序更易于与操作系统的输入/输出函数之间建立接口,一般来说它们都通过指针来通信。

如果不想将文件名定义成一个字符串字面量,可以将文件名存储在string类型的变量中,然后使用c_str成员函数。示例程序见下:

//处理文件的读写
#include <iostream>
#include <fstream>
#include <string>

using std::cout;
using std::cin;
using std::endl;
using std::ifstream;
using std::ofstream;
using std::string;

int main(int argc, char** argv){
    string file = "/Users/macbookpro/CLionProjects/ACM/infile.txt";
    ifstream infile(file.c_str());
// ifstream infile("/Users/macbookpro/CLionProjects/ACM/infile.txt");//等价于上面的两行
    ofstream outfile("/Users/macbookpro/CLionProjects/ACM/outfile.txt");

    string s;

    while(getline(infile, s)){
        outfile <<s  <<endl;
    }
    return 0;
}

从main函数参数中复制一个或多个文件:

//mian函数的参数
#include <iostream>
using std::cout;
using std::cin;
using std::endl;
using std::cerr;
#include <fstream>
using std::ifstream;

#include <string>
using std::string;

int main(int argc, char** argv){
    int fail_count = 0;
    for (int i = 1; i < argc; ++i) {
        ifstream in(argv[i]);

        if(in){//如果文件存在,则复制文件内容,
            string s;
            while(getline(in, s)){
                cout << s << endl;
            }
        }else{//否则生成一个错误信息
            cerr << "canot open file" << argv[i] << endl;
            ++fail_count;
        }
    }
    return fail_count;
}

6 内存管理的三种方式

6.1自动管理内存

这种方法常与局部变量联系在一起。

一个局部变量只在程序执行到该变量定义时才由系统自动分配内存给它,当包含该变量的定义的模块结束时,该变量占用的内存自动释放。

在下面的程序中,该函数返回一个局部变量的地址,但是在函数返回时,定义的局部变量x的语句块也同时被终止,x所占的内存被释放。因此&x创建的指针现在是无效的,将返回一个不可预料的值。

int* invalid_pointer(){
	int x;
	return &x;//灾难
}

可以声明一个静态变量x来解决刚才的问题。系统对变量x只进行一次内存的分配,之后直到程序结束之前该变量占用的内存都不会被释放。

int* pointer_to_static(){
	static int x;
	return &x;//合法
}

但是静态分配内存有缺点,每次对一个指向一个静态变量的指针的调用都会返回指向同一个对象的指针。

如果要定义一个函数,每次调用该函数都会返回一个指向特定的新的对象的指针,并且该对象一直存在到我们不再需要它。为了达到这样的目的,可以使用动态分配内存。

6.2 为对象分配/释放内存

如果T为一个对象的类型,则new T表达式将为一个T类型的对象分配内存,该变量由构造函数对其初始化,并产生一个指向该新分配内存的对象(该对象没有命名)的指针。

执行new T(args)这样的初始化语句可以给变量赋予一个特定的值。

new创建的对象将一直存在直到程序结束或者执行了delete p语句。(p为new语句中返回的一个指针)

为了执行delete删除一个指针,这个指针必须是指向一个用new语句分配内存的对象,或者是一个零指针。删除一个零指针不进行任何操作。

//示例程序

int* p = new int(42);
++ *p;//p等于43现在
delete p;//删除new创建的对象,但是指针并没有被删除,现在指向一个不可预知的值

6.3 为数组分配/释放内存

如果T是一个类型名,而n是一个非负整数,那么new T[n]语句将会为一个拥有n个T类型对象的数组分配内存,并返回一个指向数组首元素的指针(该指针类型为T*)。

每个对象都将被默认初始化,也就是如果T是内建类型而且数组又在局部作用域内分配内存,那么对象将不会被初始化,如果T是一个类,那么数组中的每个元素都将会运行类的默认构造函数进行初始化。

如果T是一个自定义类型,要注意:

  • 1 如果该类不允许默认初始化,那么编译器将终止程序;
  • 2 数组中n个元素的每一个元素都会被初始化,这将带来一定的运行是开销。

使用new T[n]来为一个数组分配内存时,如果n值是零,那么子数组中将不包含任何元素。

发生这种情况时,new函数无法返回一个指向首元素的指针(数组根本没有元素)。实际上new函数此时会返回一个有效但无意义的off-the-end指针,可以将作为delete[]的参数使用。

在下面的示例程序中,n等于0时,仍可以执行。

    int n = 0;
    int* p = new int[n];//会返回一个有效但无意义的off-the-end指针
    //可以想象成一个指向(如果存在的话)首元素的指针
    vector<int> v(p, p + n);
    delete [] p;

对delete[]的使用,[]括号在这里必不可少,它告诉系统释放整个数组占用的内存,而不仅是释放一个元素的内存。

一个元素一旦用new[]分配内存,该内存将一直被使用到程序终止或者在程序中执行了delete[]p语句(p是new[]语句返回的指针的一个副本)。

释放数组之前,系统会根据相反的顺序逐个释放数组中的每个元素。

示例程序,duplicate_chars函数使用一个指向一个空字符结尾的字符数组(例如一个字符字面量)的指针,将数组中的每个元素(包括结尾的空字符)复制到一个新分配的数组中,然后返回指向这个新数组首元素的指针。

示例程序:

//为数组分配释放内存
#include <iostream>
#include <cstddef>
#include <string>
using std::cout;
using std::cin;
using std::endl;

size_t strlen(const char* p){
    size_t size = 0;
    while(*p++ != '\0'){
        ++size;
    }
    return size;
}

template<class In, class Out>
Out copy(In begin, In end, Out dest){
    while (begin != end){
        *dest++ = *begin++;
    }
    return dest;
}

char* duplicate(const char* p){
    size_t length = strlen(p) + 1;//为空字符预留空间'\0'
    char* result = new char[length];

    copy(p, p + length, result);
    return  result;
}


int main(int argc, char** argv){
    char str[] = {'i', 'l', 'o', 'v', 'e','U'};
    std::string s =  duplicate(str);
    cout << s;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

繁星蓝雨

如果觉得文章不错,可以请喝咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值