homework-08

现代程序设计第八次作业

1. 理解C++变量的作用域和生命周期

对一个C++变量来说,有两个属性非常重要:作用域和生命周期,它们 从两个不同的维度描述了一个变量--时间和空间。顾名思义,作用域就 是一个变量可以被引用的范围,如:全局作用域、文件作用域、局部作 用域;而生命周期就是这个变量可以被引用的时间段。不同生命周期的 变量,在程序内存中的分布位置是不一样的。一个程序的内存分为代码 区、全局数据区、堆区、栈区,不同的内存区域,对应不同的生命周期。

  • 全局变量
    • 作用域:全局作用域(全局变量只需在一个源文件中定义,就可以作用于所有的源文件。)
    • 生命周期:程序运行期一直存在
    • 引用方法:其他文件中要使用必须用extern 关键字声明要引用的全局变量。
    • 内存分布:全局数据区
    • 注意:如果在两个文件中都定义了相同名字的全局变量,连接出错:变量重定义
  • 全局静态变量
    • 作用域:文件作用域(只在被定义的文件中可见。)
    • 生命周期:程序运行期一直存在
    • 内存分布:全局数据区
    • 定义方法:static关键字,const 关键字
    • 注意:只要文件不互相包含,在两个不同的文件中是可以定义完全相同的两个静态变量的,它们是两个完全不同的变量
  • 静态局部变量
    • 作用域:局部作用域(只在局部作用域中可见)
    • 生命周期:程序运行期一直存在
    • 内存分布:全局数据区
    • 定义方法:局部作用域用中用static定义
    • 注意:只被初始化一次,多线程中需加锁保护
  • 局部变量
    • 作用域:局部作用域(只在局部作用域中可见)
    • 生命周期:程序运行出局部作用域即被销毁
    • 内存分布:栈区
    • 注意:auto指示符标示
局部变量的生命周期
char *getstr()
{
    char s[20] = "hello world";
    return s;
}
int main()
{
    printf("%s",getstr());
    return 0;
}

编译时即会给出警告:

warning: function returns address of local variable [enabled by default]

由于局部变量s在getstr结束后即被销毁,输出结果为乱码。

2.理解堆和栈,两种内存的申请和释放的方式

一、内存分配方式
  1. 全局变量和静态变量(static变量),是由编译器自动分配和释放的,初始化的全局变量和静态变量放在同一块内存区中,未初始化的全局变量和静态变量则放在相邻的另外一块内存区中。
  2. 栈,是由编译器自动分配和释放的,主要是函数体的地址,参数和局部变量,静态变量不包含其中,操作方式类似于数据结构中的栈。
  3. 堆,是由程序员手动完成申请和释放的,像malloc和new,程序员没有手动释放的话,当程序结束时由系统释放没有释放的空间,其实现方式与数据结构中的堆完全不同,此时的堆的实现方式有些类似于数据结构中的链表。
  4. 程序代码区,用于存放程序的二进制代码的空间。
  5. 文字常量区,像常量字符串等存放在这里,程序结束后由系统释放。
二、内存分配中堆和栈的区别
  1. 申请释放方式
    • 栈是编译器自动申请的,例如在主函数里面,要声明 一个int变量a,那么编译器就自动开辟一块内存存放 变量a。
    • 而堆则不相同,是由程序员手动申请的,只要程序员 感觉程序此处需要用到多大的内存空间,那么就使用 malloc或者new来申请固定大小的内存使用。
    • 栈的空间在程序结束的时候由系统或者编译器自动释 放,而堆则在程序结束前由程序员手动使用delete释 放,或者忘记手动释放,由系统在程序结束的时候自 动回收。
  2. 申请后系统的相应
    • 栈,只要栈剩余的空间大小比申请的空间小,系统就自动为其分配空间,否则就会报错说明栈空间溢出。
    • 堆,首先要知道操作系统中有一个存放空闲存储块的链表,当程序员申请空间的时候,系统就会遍历整个链表,找到第一个比申请空间大的空闲块节点,系统会将该空闲块从空闲链表中删除,分配给程序,同时系统会记录这个空闲块的首地址和申请的大小,当程序员使用delete释放该空间的时候能够找到该存储区。另外,申请的空间不一定与找到的空闲块大小相同,多出来剩余的空闲区会被系统重新添加到空闲链表中。
  3. 申请的限制
    • 栈,是一种向低地址扩展的数据结构,并且是连续的存储空间,所以栈顶和栈的最大容量是固定的,在windows下,栈的最大容量是2m或者是1m,是在编译的时候就已经确定的,当申请空间大于栈的剩余空间的时候,就会报错说明overflow,所以栈能够申请的空间是比较有限的。
    • 堆,是一种向高地址扩展的数据结构,并且是不连续的,因为系统采用的是链表的方式存放空闲存储块,当然是不连续的,链表的遍历方向是由低向高的,所以堆能够申请的空间的大小其实等同于整个系统的虚拟内存,只要还有内存空间,那么堆就能够不受限制的申请空间,这种方式比较灵活,申请空间也较大。
  4. 申请效率的比较
    • 栈,因为栈空间的申请是由系统自动完成的,所以速度快,但是不受程序员控制。
    • 堆,空间的申请是由malloc或new来完成的,实现起来较慢,能够产生碎片,但是使用起来方便。
  5. 存放内容
    • 栈,栈存放的内容,一般来说是函数地址和相关参数。当主函数要调用一个函数的时候,要对当前断点进行保存,需要使用栈来实现,首先入栈的是主函数下一条语句的地址,然后是调用函数的参数,一般情况下是按照从右向左的顺序入栈,之后是调用函数的局部变量,注意静态变量是存放在全局内存区,是不入栈的;出栈的顺序正好相反,最终栈顶指向主函数下一条语句的地址,主程序又从该地址开始执行。
    • 堆,一般情况堆顶使用一个字节的空间来存放堆的大小,而堆中具体存放内容是由程序员来完成的。
堆和栈两种内存申请方式
//main.cpp
int a = 0; //全局初始化区
char *p1;  //全局未初始化区
main()
{
    int b;              //栈
    char s[] = "abc";   //栈
    char *p2;           //栈
    char *p3 = "123456"; //"123456\0"在常量区,p3在栈上。
    static int c =0;    //全局(静态)初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20); //分配得来得10和20字节的区域就在堆区。
    strcpy(p1, "123456");    //"123456\0"放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。
}

总的来说,栈是系统控制的,所有非静态局部变量(生命周期不是整个程序的)都会放在栈中。 回想一下递归调用就能明白,生命周期短就意味着需要对每个上下文保存一次局部变量。 这也是为什么尽量少用递归的原因:栈容量非常有限。而堆可以由程序员自由 分配,比较自由。相应的则更要注意释放。

3. 理解unique_ptr和shared_ptr

unique_ptr

unique_ptr是一个独享所有权的智能指针,它提供了一种严格语义上的所有权,包括:

  1. 拥有它所指向的对象。
  2. 无法进行复制构造,也无法进行复制赋值操作。也就是说,我们无法得到指向同一个对象的两个unique_ptr。但是可以进行移动构造和移动赋值操作。
  3. 保存指向某个对象的指针,当它本身被删除释放的时候(比如,离开了某个作用域),会使用给定的删除器释放它指向的对象。

使用unique_ptr,可以实现以下功能,包括:

  1. 为动态申请的内存提供异常安全。
  2. 将动态申请内存的所有权传递给某个函数。
  3. 从某个函数返回动态申请内存的所有权。
  4. 在容器中保存指针。
  5. 所有auto_ptr应该具有的(但无法在C++ 03中实现的)功能。

下面是一段传统的会产生不安全异常的代码:

X* f()
{
    X* p = new X;
    // 做一些事情,可能会抛出某个异常
    return p;
}

解决方法是,使用unique_ptr来管理这个对象的所有权,由其进行这个对象的释放工作。

X* f()
{
   unique_ptr<X> p(new X);
   // 做一些事情,可能会抛出异常
   return p.release();
}

如果程序执行过程中抛出了异常,unique_ptr就会释放它所指向的对象。但是,除非我们真的需要返回一个内建的指针,我们还可以返回一个unique_ptr。

unique_ptr<X> f()
{
    unique_ptr<X> p(new X);
    // 做一些事情,可能会抛出异常
  return p;
}

现在,我们可以这样使用函数f():

void g()
{
    unique_ptr<X> q = f();              // 使用移动构造函数(move constructor)
    q->DoSomething();                   // 使用q
    X x = *q;                           // 复制指针q所指向的对象
}    // 在函数退出的时候,q以及它所指向的对象都被删除释放

unique_ptr具有移动语义,所以我们可以使用函数f()返回的右值对q进行初始化,这样就简单地将所有权传递给了q。

shared_ptr

1.shared_ptr的线程安全性

shared_ptr 本身不是 100% 线程安全的。它的引用计数本身是安全 且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据 成员,读写操作不能原子化。根据文档,shared_ptr 的线程安全 级别和内建类型、标准库容器、string 一样,即:

  • 一个 shared_ptr 实体可被多个线程同时读取;
  • 两个的 shared_ptr 实体可以被两个线程同时写入,“析构”算写操作;
  • 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁。

2.shared_ptr用法

示例一:

shared_ptr<int> sp(new int(10));                //一个指向整数的shared_ptr    
assert(sp.unique());                            //现在shared_ptr是指针的唯一持有者     
shared_ptr<int> sp2 = sp;                       //第二个shared_ptr,拷贝构造函数     
assert(sp == sp2 && sp.use_count() == 2);       //两个shared_ptr相等,指向同一个对象,引用计数为2    
*sp2 = 100;                                     //使用解引用操作符修改被指对象    
assert(*sp == 100);                             //另一个shared_ptr也同时被修改     
sp.reset();                                     //停止shared_ptr的使用    
assert(!sp);                                    //sp不再持有任何指针(空指针)  

示例二:

class shared                                    //一个拥有shared_ptr的类    
{    
private:        
    shared_ptr<int> p;                          //shared_ptr成员变量    
public:        
    shared(shared_ptr<int> p_):p(p_){}          //构造函数初始化shared_ptr        
    void print()                                //输出shared_ptr的引用计数和指向的值        
    {          cout << "count:" << p.use_count()                
    << "v =" <<*p << endl;        
    }    
};    
void print_func(shared_ptr<int> p)                //使用shared_ptr作为函数参数    
{         
    //同样输出shared_ptr的引用计数和指向的值        
    cout << "count:" << p.use_count()            
        << " v=" <<*p << endl;  }    
int main()    
{        
    shared_ptr<int> p(new int(100));        
    shared s1(p), s2(p);                        //构造两个自定义类         
    s1.print();        
    s2.print();         
    *p = 20;                                    //修改shared_ptr所指的值        
    print_func(p);        
    s1.print();    
}   

3.应用于标准容器

有两种方式可以将shared_ptr应用于标准容器(或者容器适配器等 其他容器)。

一种用法是将容器作为shared_ptr管理的对象,如shared_ptr<list>,使 容器可以被安全地共享,用法与普通shared_ptr没有区别,我们不再讨论。

另一种用法是将shared_ptr作为容器的元素,如vector<shared_ptr >,因 为shared_ptr支持拷贝语义和比较操作,符合标准容器对元素的要求,所 以可以实现在容器中安全地容纳元素的指针而不是拷贝。

标准容器不能容纳auto_ptr,这是C++标准特别规定的(读者永远也不 要有这种想法)。标准容器也不能容纳scoped_ptr,因为scoped_ptr不 能拷贝和赋值。标准容器可以容纳原始指针,但这就丧失了容器的许 多好处,因为标准容器无法自动管理类型为指针的元素,必须编写额 外的大量代码来保证指针最终被正确删除,这通常很麻烦很难实现。

存储shared_ptr的容器与存储原始指针的容器功能几乎一样,但shared_ptr为程序员做了指针的管理工作,可以任意使用shared_ptr而不用担心资源泄漏。

下面的代码示范了将shared_ptr应用于标准容器的用法:

#include <boost/make_shared.hpp>   
int main()    
{        
    typedef vector<shared_ptr<int> > vs;    //一个持有shared_ptr的标准容器类型        
    vs v(10);                               //声明一个拥有10个元素的容器,元素被初始化为空指针         
    int i = 0;        
    for (vs::iterator pos = v.begin(); pos != v.end(); ++pos)        
    {            
        (*pos) = make_shared<int>(++i);     //使用工厂函数赋值            
        cout << *(*pos) << ", ";            //输出值        
    }        
    cout << endl;         
    shared_ptr<int> p = v[9];        
    *p = 100;        
    cout << *v[9] << endl;    
}   

这段代码需要注意的是迭代器和operator[]的用法,因为容器内存储 的是shared_ptr,我们必须对迭代器pos使用一次解引用操作符*以获 得shared_ptr,然后再对shared_ptr使用解引用操作符*才能操作真 正的值。*(*pos)也可以直接写成**pos,但前者更清晰,后者很容 易让人迷惑。vector的operator[]用法与迭代器类似,也需要使 用*获取真正的值。

总结

从别处摘了这么多,不得不承认我有很多没看懂,不过大概理解了这两种 智能指针的优势,即安全。unique_ptr保证了操作该对象的指针永远只有这 一个,所以什么时候该释放很容易确定;而shared_ptr也通过引用计数的方式 实现了对象的安全释放。

4.分割url

毫无疑问,split应该是任何非反人类的标准库编写者都应该提供 的功能,因为实在是太常用了。C++没有。那个strtok难看的一*。 而且我实在是不明白‘“C++0x”,“C++11 & STL”两种不同的代码风格’是怎样 高大上的风格,既然老师上课说了可以用自己喜欢的语言,那我 就不墨迹了。下面是ruby代码:

def urlcut(str)
    str.gsub(/[.\/]|(:\/\/)/, " ")
end
puts urlcut("http://msdn.microsoft.com/en-us/library/vstudio/hh279674.aspx")

转载于:https://www.cnblogs.com/zjoe/p/3427969.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值