C++/C 使用过程中的经验总结荟萃(持续更新)

Posted 陆嵩

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++/C 使用过程中的经验总结荟萃(持续更新)相关的知识,希望对你有一定的参考价值。

同一个算法,不同的人写出来,效率上可能会差1000倍。写程序的同时,要注意性能。

玄学

所谓的玄学,主要发生在有些情况下你写的程序压根就是错的,只不过当前的编译环境下因为某些不知名的原因没识别出错误,换个环境就报错了。不同的编译环境对于错误的包容方面是不一样的,有的环境下,这种错误不识别,有的环境下,那种错误不识别。总之,你就是写错了的,编译器没认出来。所以说,规范编程很重要,不要感觉,编译和运行不报错就可以乱来。

命名空间

使用命名空间可以有效地防止命名冲突

左值非const

  • 在C++语言中,对于一个由类名加俩冒号再加成员名构成的东西(学名叫“quilified-id”),比如:A::x,只有当x是A类的静态成员的时候,A::x才能表示一个左值。

  • 对于函数类型到函数指针类型的默认转换,只有当函数类型是左值的时候才行。所有对于非静态的成员函数,就不存在这种从函数类型到函数指针类型的默认转换,于是编译器也就不知道这个

结构体 set 和 map

set中放入结构体或者自定义对象时,结构体的写法,要重载小于号,注意是 bool operator<(const A & )const 的形式(map一样要重载)。关于比较函数,return false 就是交换,直到任意两个输入,走一遍都能return true。具体怎么比的,就很迷。

STL 容器选择

  • C++ 的 STL 容器主要分为序列式和关联式。所谓的序列式宏观上理解就是小鬼们按一定的顺序排排坐。所谓关联式类似于数据库里面,有一个 key,有一个值这样的。序列式的容器包含 array、vector、list、deque,关联式的容器有 map、set 和 multi 以及 unordered 两个层面的变种,除此之外,还有在学数据结构时,耳熟能详的栈(stack)、队列(queue)等。
  • 很多人写算法,想到哪个就用哪个,这是很不对的,在选择容器的时候,我们要考虑程序的性能。那么应该如何选择合适的容器呢?如果你想表达的数据,stack 或者 queue 的特征已经非常明显了,直接用他俩;如果对数据后续要有大量的查找,就用关联式容器,其中又以无序的查找最快,但是它无序;如果有大量的添加和删除操作(特别是在中间),对次序有要求,选择 list,而尽可能地避免 vector 和 array;如果对元素的次序要求比较高,而且有随机访问(所谓的随机访问,是说有按下标比如说v[3],v[5]这样访问的),且没有元素在中间的插入或者删除,且没有极其大量的查找,可以选择 vector 和 array…知道或者大概知道长度的,尽量用array。

关于-O3的过度优化

O3的优化很神奇,他会自动判断你哪些代码是无效,比如说某个循环不运行可能对结果没影响,她就会给你忽略过去。

开O3优化会让你产生一些错觉。找热点和调试程序的时候尽量不要开-O3优化。主要原因是数据区用满了,-O3优化下,不会报错,不同的全局变量之间,相互污染了。

内存上的性能优化

内存尽量整一块,不要东找找,西找找。任务集中,相同的任务放在一起做。

简单地说,如果你哪个变量在全局变量开太大,导致 bss 越界了,就可以用 malloc 动态开辟内存,虽然速度慢点,但是没有问题。

全局的未初始化变量存在于.bss段中,具体体现为一个占位符。全局的已初始化变量存于.data段中。

系统允许你的数据是 1G ,那么这1G = 已初始化数据 + bss。

关于 set 和 map

  • map和unordermap更适合建数据库,以供查询。

  • 用bool向量来替代删除,有时候可以避免内存的开辟和变量的迁移。用什么数据结构内存占用等方面本质相同的。差距在于操作时是否有内存的开辟,以及数据的拷贝。

  • 别一开始就定义很完整的结构体,先做主要工作,后面需要什么再补上。

  • 结构体set得自己重载比较规则。

  • set map虽然查找很快,但是建立非常慢,适合只会增加出现,很少会没的东西。

  • 容器的insert其实一种拷贝,而不是引用。

  • 能不用指针的,尽量别用。

  • map和set是插入的时候才排序。如果过程中有值的改变,是不会进行重排序的。

const_cast的妙用

学会用 const_cast 处理一些报错。

关于函数传递数组与指针

  • 同一个变量的多次声明,都指向一个地址,比如,你定义了 int a多次,其实都是指向一个地址。

  • 一个经验总结就是:c++能不用指针就不用指针,尽量用引用。指针我们是用不好的,尤其是多重指针。

  • 存数组,不能把临时数组的指针当成这个属于存下来,否则你下次指针指向的内容改变了,数组也就变了。同一个临时变量的多次重新声明和定义,地址总是不变的,并不会开辟新的内存。

匿名表达式

C++ 11 新特性,用多了是不是很酷。
[=] (int x, int y) -> bool return x%10 < y%10;
我们可以看到,匿名表达式无非就是一般的函数把函数名隐藏,然后把返回类型用->放到后面。加了一个中括号,表示捕获列表,即从外部传入函数体内的一些变量,包括引用和非引用方式的传入。= 就是非引用传入,& 就是引用传入,值可以被修改。也可以单独地传入某些个外部变量。

Debug 模式和 Release 模式

一般来说,release 模式跑起来更快。但是,有时写内存访问冲突问题,release 模式下不报错,但是再在debug 模式下会报错,也有的时候有问题也都不报错。删除了一个数之后,for 循环的迭代器是否会继续往后走,是未知的,有时候会,有时候不会。C++ 的内存这个东西,是个玄学,保证代码正确即可,不必深究。如:

int main()

	vector<int> a =  7,6,5,4,3,2,1 ;
	for (auto& it : a)
	
		cout << "it:" << it << endl;
		if (it == 6)
		
			for (auto ite = a.begin(); ite != a.end(); ite++)
			
				if (*ite == it)
					a.erase(ite);
			
			cout <<"after del:" << it << endl;

			
	
	return 0;

全局变量

vector<int> a(10); 等价于 vector<int> a(10,0);存放于存于.data段中。

Ubuntu 修改C头文件(链接库、环境变量)目录

最直接的就是在/etc/profile添加,然后关机重启,部分情况下 source 一下也可以。LD_LIBRARY_PATH 是动态链接库,LIBRARY_PATH 是静态链接库。目录只会在给定的路径下查找,而不会递归地查找子文件夹。冒号拼接,最后不加斜杆。看清楚需要添加的路径是哪个,不要少加了几层,也不要多加了几层。

export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/include/libxml2:/home/test/Desktop/boost_1_75_0
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/include/libxml2:/home/test/Desktop/boost_1_75_0
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/test/Desktop/boost_1_75_0/stage/lib  
export LIBRARY_PATH=$LIBRARY_PATH:/home/test/Desktop/boost_1_75_0/stage/lib  

类中vector的初始化

在类XXX中创建一个10个元素的vector A, 初始值为0:

class XXX
public:
	vector<int> A(10, 0);
......	

报错expected identifier before numeric constant,意为:数字常量前应有标识符。原因: 编译器认为, 你正在定义一个成员函数,函数名为A ,返回值类型为 vector ,函数有两个输入参数(10, 0), 但是参数的给定方式有错误。修改:

class XXX
public:
	vector<int> A = vector<int> (10, 0);
......	

vector在使用迭代器遍历过程中,不能使用erase (),否则出错

关于类型重命令

vector带大小不代表一种类型,所以typedef std::vector<double>(3) v3type;是不合法的。C++ 11的标准下可以使用using identifier = type;替代typedef。

explicit关键字

C++提供了关键字 explicit,使用 explicit 关键字,便不允许构造函数的隐式转换调用。如下,t3 的构造是会报错的,因为explicit,规定了 Test2 的构造只能使用默认的构造。

#include <iostream>
using namespace std;
class Test1

public :
	Test1(int num):n(num)
private:
	int n;
;
class Test2

public :
	explicit Test2(int num):n(num)
private:
	int n;
;
 
int main()

	Test1 t1 = 12;
	Test2 t2(13);
	Test2 t3 = 14;
		
	return 0;

static_cast

基本类型之间的转换,但不能用于基本类型指针之间的类型转换(void指针和基本类型指针之间可以)。

 double d=0;
 int i=static_cast<int>(d);

关于 const 成员函数重载

如果类实例为const,就调用的const函数,否则调用非const。直接看例子。

using namespace std;  
   
class Test  
  
protected:  
    int x;  
public:  
    Test (int i):x(i)    
    void fun() const  
      
        cout << "fun() const called " << endl;  
      
    void fun()  
      
        cout << "fun() called " << endl;  
      
;  
   
int main()  
  
    Test t1 (10);  
    const Test t2 (20);  
    t1.fun();  //此处调用fun()  
    t2.fun();  //此处调用 fun() const
    return 0;  

函数模板类型推断不能通过左值

在推断函数模板的类型时,类型必须显示地出现在函数的调用表达式里面,否则会出错。如:

template <Uint _Size>
inline SVectorCL<_Size> std_basis(Uint i)

    SVectorCL<_Size> ret(0.);
    if (i>0) ret[i-1]= 1.;
    return ret;


//正确调用
SVectorCL<5> myVector9 = std_basis<5>(2);
//错误调用
SVectorCL<5> myVector9 = std_basis(2);

关于枚举类型

所谓的枚举,本质就是限定一个变量只能取一些离散的整数值,为了增加可读性,往往会找一些字母串来表示作为这些整数值得代号。枚举变量的值只能取枚举常量表中所列的值,就是整型数的一个子集。基本的写法如下:

enum color_set1 RED, BLUE, WHITE, BLACK; // 定义枚举类型color_set1
enum week Sun, Mon, Tue, Wed, Thu, Fri, Sat; // 定义枚举类型week

关于传入参数的默认值

一般情况下,在函数调用时形参从实参那里取得值,因此实参的个数应与形参相同。有时多次调用同一函数时用同样的实参,C++ 提供简单的处理办法,给形参一个默认值,这样形参就不必一定要从实参取值了。这种方法比较灵活,可以简化编程,提高运行效率。

关于模板

处理模板函数和类模板时,因为要进行实例化模板函数和类模板,要求编译器在实例化模板时必须在上下文中可以查看到其定义实体。因为编译器不会会预先知道 typename 实参是什么呢。因此模板的实例化与定义体必须放到同一翻译单元中。也就是 h 文件和 cpp 分离式的写法,模板函数实现最好写在 h 文件中。某个类的成员函数,如果是模板函数,也要和声明定义写在一块。

头文件定义

定义头文件都要养成一个好习惯,

#ifndef DROPS_FEMP3_H
#define DROPS_FEMP3_H
xxx
#endif

防止重复调用带来的重定义。

for_each

模板函数原型:

template<typename InputIterator, typename Function>
Function for_each(InputIterator beg, InputIterator end, Function f) 
  while(beg != end) 
    f(*beg++);

for_each 的返回值是其第三个参数。
用法示例:

# include<iostream>
# include<algorithm>
# include<vector> 
 
using namespace std;
 
void Print(int val)

	cout << val << " ";

 
/*
基本数据类型 
遍历算法
*/
void test1()

	vector<int> v;
	
	v.push_back(1);
	v.push_back(5);
	v.push_back(4);
	v.push_back(2);
	v.push_back(6);
	
	for_each(v.begin(),v.end(),Print);
  
 
/*
自定义数据类型
for_each遍历 
*/ 
class Person
	public:
		Person(int a,int b):index(a),age(b)
	public:
		int age;
		int index;
;
 
void Print2(Person &p)

	cout << "index:" << p.index << " " << "age:" << p.age << endl;

 
void test2()

	vector<Person> vp;
	Person p1(1,4),p2(2,5),p3(3,6);
	vp.push_back(p1);
	vp.push_back(p2);
	vp.push_back(p3);
	
	for_each(vp.begin(),vp.end(),Print2); 

 
int main()

	//test1();
	test2();
 
 return 0;

关于 mem_fun

mem_fun、mem_fun_ref 的作用就是将一个"成员函数指针"包装成一个仿函数。当容器中存放的是对象实体的时候用 mem_fun_ref,当容器中存放的是对象的指针的时候用 mem_fun 。

class  ClxECS

public :
     int  DoSomething() 
     
         //  这里以输出一句话来代替具体的操作
        cout  <<   " Output from method DoSomething! "   <<  endl; 
         return   0 ; 
    ;
;

vector < ClxECS *>  vECS;

for ( int  i  =   0 ; i  <   13 ; i ++ )

    ClxECS  * pECS  =   new  ClxECS;
    vECS.push_back(pECS);


int  DoSomething(ClxECS  * pECS)

     return  pECS -> DoSomething();


for_each(vECS.begin(), vECS.end(),  & DoSomething);//方式1
for_each(vECS.begin(), vECS.end(), mem_fun( & ClxECS::DoSomething));//方式2

上述方式 1 访问需要单独定义一个全局的仿函数。要直接调用 ClxECS 中对应的成员函数,才有了方式 2。
DoSomething 函数是无参的,而对于有一个参数的函数,可以使用std::bind 辅助函数。

std::bind2nd

std::bind1st 和 std::bind2nd 函数用于将一个二元算子转换成一元算子。bind 的意思是“绑定”,1st 代表 first,2nd 代表 second。 例子如下:

#include <iostream>
#include <functional>
#include <algorithm>
#include <vector>
using namespace std;
 
int main()

    vector<int> coll =  1,2,3,5,15,16,18 ;
 
    // 查找元素值大于10的元素的个数
    // 也就是使得10 < elem成立的元素个数 
    int res = count_if(coll.begin(), coll.end(), bind1st(less<int>(), 10));
    cout << res << endl;
 
    // 查找元素值小于10的元素的个数
    // 也就是使得elem < 10成立的元素个数 
    res = count_if(coll.begin(), coll.end(), bind2nd(less<int>(), 10));
    cout << res << endl;
 
    return 0;
 

std::transform

std::transform在指定的范围内应用于给定的操作,并将结果存储在指定的另一个范围内。要使用std::transform函数需要包含<algorithm>头文件。

template <class InputIterator, class OutputIterator, class UnaryOperation>
  OutputIterator transform (InputIterator first1, InputIterator last1,
                            OutputIterator result, UnaryOperation op);
	
template <class InputIterator1, class InputIterator2,
          class OutputIterator, class BinaryOperation>
  OutputIterator transform (InputIterator1 first1, InputIterator1 last1,
                            InputIterator2 first2, OutputIterator result,
                            BinaryOperation binary_op);

类成员变量的初始化

类成员变量的初始化,一般都放在构造函数里面,类内不能直接初始化。如下写法是错误的。

class Solution 
    vector<int> r(10);
;

正确的写法是通过构造函数初始化。

class Solution
Type() : vec(10)   // 要这么初始化
vector<int> vec;
;

因为类只是定义的类型,还没有实例化,也就是没有定义类的对象(变量),没法存储,你可以在初始化列表里进行初始化。

自定义默认代码

#include <iostream>
using namespace std;
int main()


    return 0;

valarry

valarray 是一个类模板,面向数值计算,优势是重载了各种数学函数,方便数学计算。
valarray::resize 将 valarray 中的元素数更改为指定数量,如果第二个传入参数省略,默认赋值为 0。

unique_ptr

动态内存的使用很容易出问题,因为确保在正确的时间释放内存是极其困难的。有时会忘记释放内存,在这种情况下会产生内存泄露和非法指针的引用。而智能指针,当该对象被销毁时,会在其析构函数中删除关联的原始指针。就不用再专门地删除对象了。

为了更容易(同时也更安全)地使用动态内存,C++11标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指的对象。C++11标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr 允许多个指针指向同一个对象,unique_ptr 则"独占"所指向的对象。

它的用法就是把指针形如 Task* 改成 std::unique_ptr<Task>

accumulate 自定义数据类型的处理

四个形参:头两个形参指定要累加的元素范围,第三个形参则是累加的初值,第四个回调函数来实现自定义数据的处理。

#include <vector>
#include <string>
using namespace std;
 
struct Grade

	string name;
	int grade;
;
 
int main()

	Grade subject[3] = 
		 "English", 80 ,
		 "Biology", 70 ,
		 "History", 90 
	;
 
	int sum = accumulate(subject, subject + 3, 0, [](int a, Grade b)return a + b.grade; );
	cout << sum << endl;
 
	system("pause");
	return 0;

dynamic_cast

dynamic_cast 运算符的主要用途:将基类的指针或引用安全地转换成派生类的指针或引用,并用派生类的指针或引用调用非虚函数。如果是基类指针或引用调用的是虚函数无需转换就能在运行时调用派生类的虚函数。

前提条件:当我们将 dynamic_cast 用于某种类型的指针或引用时,只有该类型至少含有虚函数时(最简单是基类析构函数为虚函数),才能进行这种转换。否则,编译器会报错。

numeric_limits

提供了类型的一些数值极限,如:

 cout<<"numeric_limits<int>::min()= "<<numeric_limits<int>::min()<<endl; //int的最小值  
 cout<<"numeric_limits<int>::max()= "<<numeric_limits<int>::max()<<endl;  //int的最大值  

虚函数

一言以蔽之,基类类型的指针指向子类,该指针调用的同名函数是子类的。

mutable 关键字

在C++中,mutable 是为了突破 const 的限制而设置的。可以用来修饰一个类的成员变量。被 mutable 修饰的变量,将永远处于可变的状态,即使是 const 函数中也可以改变这个变量的值。

大括号

在C/C++中大括号指明了变量的作用域,在大括号内声明的局部变量其作用域自变量声明始,到大括号之后终结。我们应该善用它,使我们的程序更加清晰明白。C++ 的大括号不是无意义,它圈定了大括号里面变量的生存周期。

typename 关键字

有时候,在使用模板参数,容易引起歧义的时候,我们会在前面加个 typename 关键字,说明后面是个类类型,而不是别的东西。

const

c++ 函数前面和后面 使用const 的作用:

  • 前面使用const 表示返回值为const
  • 后面加 const表示函数不可以修改class的成员

以上是关于C++/C 使用过程中的经验总结荟萃(持续更新)的主要内容,如果未能解决你的问题,请参考以下文章

C++/C 使用过程中的经验总结荟萃(持续更新)

Python_学习经验总结2018.2.17持续更新

C#Java中的一些小功能点总结(持续更新......)

AI笔记优质文章分享(持续更新)

Leecode题解汇总(持续更新)

CC++概念性面试题总结持续更新