C/C++重难点总结系列(五)


41.C++文件流操作

多态性:fstream继承自iostream,ifstream继承自istream,ofstream继承自ostream,因此参数为iostream的引用/指针的可以直接传入fstream的实参。

操作步骤:创建文件流,打开文件,读写文件,关闭文件。

操作示例:

   char data[100];
   // 以写模式打开文件
   ofstream outfile;
   outfile.open("afile.dat",app);//追加写模式打开
   cout << "Enter your name: "; 
   cin.getline(data, 100);
   // 向文件写入用户输入的数据
   outfile << data << endl;

   cout << "Enter your age: "; 
   cin >> data;
   cin.ignore();
   // 再次向文件写入用户输入的数据
   outfile << data << endl;

   // 关闭打开的文件
   outfile.close();

   // 以读模式打开文件
   ifstream infile; 
   infile.open("afile.dat"); 
   infile >> data; 
   // 在屏幕上写入数据
   cout << data << endl;
   
   // 再次从文件读取数据,并显示它
   infile >> data; 
   cout << data << endl; 

   // 关闭打开的文件
   infile.close();

42.vector的动态分配原理

在STL标准库的实现当中,vector内存不够时,会动态的把自己的容量扩展原来的1.5-2倍。这个过程他会重新申请内存,并把原来的数据拷贝到新内存当中来,并删除原来的内存。这样的拷贝效率确实有点低. 这个问题可以通过在vector当中只存储大对象的指针来优化, 但是还是存在一些不必要的拷贝。

可以使用reserve()成员设置每次重新分配的最小容量,size()获取vector当前实际大小,capacity()获取当前容量。

如:

int main()
{
    vector<int> vec_int(10,0);
    cout<<"size:"<<vec_int.size()<<" cap:"<<vec_int.capacity()<<endl;
    vec_int.insert(vec_int.end(),2,1);
    cout<<"size:"<<vec_int.size()<<" cap:"<<vec_int.capacity()<<endl;
    return 0;
}

vector的初始大小为10,插入两个1时会将内存扩展到原来的两倍,因此实际空间为12,而当前容量为20。

43.关于C++回调的实现

回调函数机制简单地说,一个A函数(调用者,如业务代码)需要实现某个功能,需要借助B函数来完成(如某个库函数),而B函数完成该功能时需要A提供一些方法和条件,这时A函数可以通过 函数指针以参数的形式提供给B函数,这个函数指针参数就是一个 回调函数, 这个回调函数会在条件满足时自动调用执行以完成该功能。
在C语言中,回调函数往往指函数指针,而C++面向对象里回调函数可以衍生成 回调对象的概念, 可调用对象均可作为回调思想的一种实现。(PS:吐槽一下,google出来所有关于C++回调的几乎都无一例外的在讲函数指针,这是过程语言里的做法,在VC时代如Windows消息机制里被大量采用。但是 我认为 C++里回调的概念应该上升至回调对象这样回调函数就与可调用对象联系起来了,回调不应该仅仅指回调函数,回调是一种思想。
C++ 中常见的可调用对象有如下几种:

(1)函数指针

回调函数最常见的应用就是STL里的泛型算法,如快排的使用就会用到回调机制。

template<typename _RAIter, typename _Compare>
    void 
    sort(_RAIter, _RAIter, _Compare);
这个原型中第三个参数即回调函数入口,需要用户提供一个比较方法,比如对于一个vector<pair<int,int> >容器,需要将其每个元素按pair的second进行降序排列,实现的比较函数如下:

bool cmpByValue(const pair<int,int> &x,const pair<int,int> &y)
{
    return x.second>y.second;
}

实现上述比较函数后,可以将函数名作为参数传入sort(),函数名传参时隐式转化为函数指针,这是函数指针实现回调的一种典型用法。

 sort(vec_test.begin(),vec_test.end(),cmpByValue);

(2)仿函数

仿函数是一个类,一个重载了operator()的类,因此该类的对象可以像函数一样被调用,因此仿函数又被称为函数对象,是C++面向对象里最初的可调用对象。

函数指针实现的回调没有函数对象优雅,如下:

class myCmp{
public:
    bool operator()(const pair<int,int> &pr1,const pair<int,int> &pr2){
        return pr1.second>pr2.second;
    }
};

(3)lambda表达式(C++ 11)

lambda表达式可以理解为一个匿名的内联函数,对于一些短小的函数体可以以表达式的形式更精简的实现,提高了代码的可读性。lambda表达式是可调用对象的一种。

值得一提的是,lambda本质是个函数,所以可以直接传入C函数的回调接口,但是仿函数本质是个对象,不能直接传入C接口的函数参数。

Lambda表达式的基本语法是:
  [上下文变量说明](Lambda表达式参数表) -> 返回类型 { 语句块 }

上下文变量说明部分就是说明对于上下文变量的引用方式,=表示值传递,&表示引用传递,

例如,&s就表示s变量采用引用传递,不同的说明项之间用逗号分隔,可以为空,但是方括号不能够省略。

第一项可以是单独的一个=或者&,表示,所有上下文变量若无特殊说明一律采用值传递/引用传递,什么都不写默认为值传递。

同样的sort()函数,以lambda表达式示例如下:

int main()
{
    vector<pair<int,int> > vec_test;
    for(int i=0;i<10;i++){
        vec_test.push_back(make_pair(i,i+1));
    }
    sort(vec_test.begin(),vec_test.end(),[=](const pair<int,int> &pr1,const pair<int,int> &pr2){
                                              return pr1.second>pr2.second;});
    for(auto e:vec_test){
        cout<<e.first<<","<<e.second<<endl;
    }
    return 0;
}

(4)std::function类(C++ 11)

function<>可以将普通函数,lambda表达式和函数对象类统一起来。它们并不是相同的类型,然而通过function模板类,可以转化为相同类型的对象(function对象)

其中模板参数为函数的签名:返回类型+参数类型。

因此,function的作用可以理解成:

通过std::function对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、函数对象等)的封装,形成一个新的可调用的std::function对象;让我们不再纠结那么多的可调用实体。一切变的简单粗暴。
  std::function最大的作用就是用来实现函数回调,比如前面提到的仿函数不能直接作为C语言回调函数的接口,但是有了function<>的“统一”效果,仿函数可以通过fucntion处理后实现回调,如下:

int main()
{
    vector<pair<int,int> > vec_test;
    for(int i=0;i<10;i++){
        vec_test.push_back(make_pair(i,i+1));
    }
    function<bool(const pair<int,int> &,const pair<int,int> &)> myFunc;
    myCmp cmp;//仿函数对象
    myFunc=cmp;//转化为function对象实体
    sort(vec_test.begin(),vec_test.end(),cmp);
    for(auto e:vec_test){
        cout<<e.first<<","<<e.second<<endl;
    }
    return 0;
}

44.关于智能指针原理

智能指针是一种典型的RAII技术的产物,即用对象管理资源,智能指针对普通指针封装并引入引用计数机制,引用计数记录多个智能指针指向同一个对象时的情况,当其减为0时释放内存。(具体实现原理及简单C++实现代码见博客:《C++智能指针原理分析与简单实现》)。

45. 关于联合体的作用与内存问题

联合的所有成员共享同一内存位置,不同成员对该内存“分时复用”。因此,当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体。
若成员具有不同大小,则联合的大小为最大成员的内存占用大小。
若成员大小相差太大,可采用指向这些成员的指针来减小空间浪费。


46.不要在构造和析构中调用虚函数

构造函数中调用虚函数不会下行至派生类,仍只会调用基类的构造函数,因为构造函数会先调用基类部分,派生类部分可能还未构造完成。因此,调用虚函数没起到作用。
同样,析构函数中调用虚函数也没有作用,析构函数是先调用派生类析构再调用基类析构,派生类部分可能已经释放掉了,虚函数仍然没有意义。

47.为何要为继承体系中的顶层基类声明一个虚析构函数

effective C++里有一条:为基类声明一个虚析构函数函数。原因呢?
这一条的目的主要是为了解决一种情况:当基类指针指向派生类对象,并通过delete指针删除这个对象时的内存泄露问题。如果不声明为虚析构,则基类指针无法调用派生类的析构函数,如果派生类里有动态数据成员,则造成内存泄露。


48.C++线程安全问题

(PS:C++三大安全问题:类型安全问题、异常安全问题、线程安全问题,到此已完结,前两个在该系列前几篇中有提及)
线程安全:单线程和多线程环境下运行结果应该保证一致。
引发原因:C++中导致线程安全问题的导火索只有两个! 静态变量(包括静态局部变量)和 全局变量
解决办法:当多个线程共享资源时应做好访问保护,以保证共享资源在同一时刻只被同一个线程访问,这种访问保护叫 线程同步,同步的含义应该是:序列化的,有秩序地访问。
注:
(1)一般来讲,只有读操作的多线程访问是安全的,只有多个线程同时出现读操作和写操作,或者多个线程同时写操作才需要同步。
(2)静态局部变量往往是最大的争议,事实上静态局部变量在初始化时不是线程安全的!除了做好同步之外,有个更加简单的解决办法:在子线程创建前,在主线程中调用一次完成初始化。
(3)STL中的容器不是线程安全的!需要手动同步。
(4)线程同步常用的方法(linux):互斥锁,条件变量,信号量,读写锁,自旋锁等(本人另一系列:linux环境编程里会专门总结多线程与多进程编程要点,待更新)。


 49.可重入与线程安全的关系(函数)

线程安全函数:函数内部存在对全局或静态变量的操作,但多线程环境下仍能得到正确的结果。(说明代码做好了线程同步)
可重入函数:某个函数中断后再次回到该函数仍能保证结果正确。
关系:可重入函数一定是线程安全的,反之不成立。举例说明如下:
(1)某个函数用到了全局或静态变量却没同步,它不是线程安全的,也不是可重入的。
(2)某个函数对使用的全局变量进行加锁,因此该函数线程安全了,但是仍不是可重入的;
  若去掉使用的共享变量而采用参数代替,则该函数可重入。

50.关于 特殊数据成员的初始化问题

有些数据成员在构造时应注意初始化的位置,且C++11引入了新的初始化方法。
(1)const变量和引用由于定义时必须初始化,因此const修饰的数据成员和引用数据成员应该在构造函数的初始化列表中完成初始化,而不能置于构造函数体内。
(初始化列表才是数据成员的初始化手段,函数体内的赋值操作不叫初始化!)
(2)C++11提出了新的初始化方法,允许非静态成员直接在声明时给一个初值,使得带const的和引用数据成员的初始化更加方便,如:
class A{
    ...
private:
    const int mVal=0;
    ...
};

(3)静态数据成员必须在类外定义,因此初始化也只能在类外定义处进行。
  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ctrlturtle

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值