【笔记】C/C++编程中的一些常用函数、功能和容易混淆的概念

记录C/C++中常用的函数、易混淆的概念和日常开发记录的笔记,仅作为自己加深印象,强化学习使用。

1. 分割字符串函数(strtok)

该函数包含在cstring中。

参考地址:http://www.cplusplus.com/reference/cstring/strtok/

这个函数在解析数据时很有用。

  • 函数原型:
char *strtok(char s[], const char *delim)

包含在头文件cstring中,s为要分解的字符串,delim为分隔符字符。s指向要分解的字符串,之后再次调用要把s设成NULL。

  • 代码示例:
#include <stdio.h>
#include <string.h>

int main ()
{
  char str[] ="- This, a sample string.";
  char * pch;
  printf ("Splitting string \"%s\" into tokens:\n",str);//
  pch = strtok (str," ,.-");//第一次拆分,使用str
  while (pch != NULL)
  {
    printf ("%s\n",pch);
    pch = strtok (NULL, " ,.-");//第二次拆分,使用NULL
  }
  return 0;
}

2. 错误时结束程序函数(assert)

assert宏的原型定义在<assert.h>中(或者cassert),该头文件也只有这一个函数。其作用是如果它的条件返回错误,则终止程序执行。原型如下:

#include <assert.h>
void assert( int expression );

assert的作用是先计算表达式 expression ,如果其值为假(即为0),那么它先向stderr打印一条出错信息,然后通过调用 abort 来终止程序运行。看下面的例子:

#include <assert.h>
#include<iostream>
using namespace std;
int main( void )
{
    assert(0&&"报错了!!!");//也可以assert(0);
    cout<<"结尾!!!"<<endl;
    return 0;
}

输出:

int main(): Assertion `0&&"报错了!!!"' failed.

说明:

  • assert括号内如果为假,那么程序直接结束;
  • assert括号中双引号的作用是为了描述错误,对功能没有影响。

3. 避免同一个头文件包含多次

有两种实现方式,一种是#ifndef方式;另一种是#pragma once方式。

  • 方式一:
#ifndef  __SOMEFILE_H__
#define   __SOMEFILE_H__

 ... ... // 声明、定义语句

#endif
  • 方式二:
#pragma once

 ... ... // 声明、定义语句

区别:

(1)#ifndef

  • #ifndef的方式受C/C++语言标准支持。它不仅可以保证 同一个文件不会被包含多次,也能保证内容完全相同的两个文件(或者代码片段)不会被不小心同时包含

  • 当然,缺点就是如果 不同头文件中的宏名不小心“撞车”,可能就会导致你看到头文件明明存在,但编译器却硬说找不到声明的状况——这种情况有时非常让人郁闷。

  • 由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,#ifndef会使得 编译时间相对较长,因此一些编译器逐渐开始支持#pragma once的方式。

(2)#pragma once

  • #pragma once 一般由 编译器提供保证:同一个文件不会被包含多次。注意这里所说的“同一个文件”是 指物理上的一个文件,而不是指内容相同的两个文件
  • 你无法对一个头文件中的一段代码作#pragma once声明,而只能针对文件。
  • 对应的缺点就是如果某个头文件有多份拷贝,本方法不能保证他们不被重复包含。当然,相比宏名冲突引发的“找不到声明”的问题,这种重复包含很容易被发现并修正。

#pragma once 方式产生于#ifndef之后,因此很多人可能甚至没有听说过。目前看来#ifndef更受到推崇。因为#ifndef受C/C++语言标准的支持,不受编译器的任何限制;

#pragma once方式却不受一些较老版本的编译器支持,一些支持了的编译器又打算去掉它,所以它的兼容性可能不够好。

4. C++中的四种强制类型转换

类型用法
static_cast1. 内置数据类型的转换;2. 继承关系的指针或引用;3. 不能用于内置数据类型指针的转换
dynamic_cast只能用于继承关系且子类转父类的指针或引用(子类转父类,不会越界)
const_cast增加或者去掉const属性,必须用于指针或引用
reinterpret_cast任何类型都可以转,包括函数指针、无关数据类型等

代码举例:

#include<iostream>
using namespace std;
class Base{};
class Derive:public Base {};
void test_01()
{
    //基本类型
    int a = 10;
    double b = static_cast<double>(a);
    //继承关系的对象指针,子类和父类可以相互转换,但是需要自己注意类型安全问题
    Base *B = nullptr;
    Derive *D = nullptr;
    D = static_cast<Derive*>(B);//注意Derive*或者Derive&
    B = static_cast<Base*>(D);
    //不能用于基础类的指针
//    int *p = nullptr;
//    char *sp = static_cast<char*>(p);
}
void test_02()
{
    //只能用于子类转父类
    Base *B = nullptr;
    Derive *D = nullptr;
    B = dynamic_cast<Base*>(D);
    //D = dynamic_cast<Derive*>(B);不可以
}
void test_03()
{
    //用于去掉const属性
    int a = 10;
    const int &b = a;
    int& c = const_cast<int&>(b);
    c = 20;//a,b,c全部修改为20
    cout<<"a: "<<a<<endl;
    cout<<"b: "<<b<<endl;
    cout<<"c: "<<c<<endl;

    //指针
    const int* p = nullptr;
    int* cp = const_cast<int*>(p);

    //增加const
    int* p1 = nullptr;
    const int* p2 = const_cast<const int*>(p1);
}

int main()
{
    test_03();
    return 1;
}

5. 在C++程序中调用被C编译器编译后的函数,为什么要加extern“C”?

对于下列函数:

void foo(int x, int y);
  • C++语言支持函数重载,C语言不支持函数重载,函数被C++编译器编译后在库中的名字与C语言的不同。
  • C编译器编译后在库中的名字为 _foo,而C++编译器则会产生像: _foo_int_int 之类的名字
  • 为了解决此类名字匹配的问题,C++提供了C链接交换指定符号 extern “C”。

6. 内存溢出 内存泄露 内存越界 堆栈溢出 野指针

  • 内存泄露

内存泄露是指程序中已动态分配的堆内存,因为某种原因未释放或无法释放内存,造成内存空间的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

  • 内存溢出

应用系统中存在 无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了,系统会提示内存溢出,有时候会自动关闭软件,重启电脑或者软件后释放掉一部分内存又可以正常运行该软件

  • 内存越界

向系统申请了一块内存,而在 使用的时候超过了申请的范围,常见的如数组访问越界

  • 堆栈溢出
  1. 堆栈溢出一般包括堆内存溢出和栈内存溢出,两者都属于缓冲区溢出。
  2. 堆内存溢出:可能是堆的尺寸设置的太小、动态申请的内存没有释放等。
  3. 栈内存溢出:可能是栈的尺寸设置的太小、递归层次太深、函数调用层次过深、分配了过大的局部变量等
  • 野指针
  • 野指针就是指针 指向的位置是不可知 的(随机的、不正确的、没有明确限制的)。指针变量在定义时如果未初始化,其值是随机的。

造成野指针的原因

  1. 指针变量没有初始化,任何刚创建的指针不会自动生成为NULL,需要手动设置。
  2. 指针被free或者delete之后没有设置NULL
  3. 指针操作超越变量作用域。所以函数中不要返回指向栈内存的指针或者引用,因为栈内存在函数结束的时候会被释放。

7. int fun() 和 int fun(void)的区别?

  • 在C++中没有区别,都是输入void类型,返回int类型的函数。
  • 在C中则有区别,这两个函数都返回int类型(即使函数前面没有int,也返回int),但是括号中没有内容表示 输入类型和个数没有限制,第二个则表示函数输入类型为void。

8. C++中的继承、封装和多态

  • 继承的目的是为了提高代码的复用性和可扩展性;
  • 封装的目的是为了保证变量的安全性,使用者不必在意具体实现细节,而只是通过外部接口即可访问类的成员;
  • 多态的目的是实现了动态联编,使程序运行效率更高,更容易维护和操作。

这也是C++和C语言区别,C语言是面向过程的语言,它的核心是函数,而C++是面向对象的语言,他的核心是类和对象。其实,C++是C语言的超集。

9. C++的内存有哪几种类型?

堆(malloc)、栈(stack)、程序代码区、全局/静态存储区、常量存储区。

  • 堆区:由 new申请分配的内存块,我们通过应用程序来动态控制它们的申请和释放。如果程序没有正确释放它们,那么程序结束后,由操作系统自动回收。

  • 栈区:由 编译器自动申请和释放的内存块,通常用来存储 局部变量、临时变量、函数参数。执行效率高,但是分配的内存容量有限。

  • 程序代码区:存放程序二进制代码。

  • 全局/静态存储区: 全局变量和静态变量是存储在一块的,程序结束后由系统释放

  • 常量存储区:存储常量的内存块,不允许被修改。

10. C++中的public、protected和private

  1. 类的一个特征就是封装,public和private作用就是实现这一目的。用户代码(类外)可以访问public成员,而不能访问private成员;private成员只能由类成员(类内)和友元访问
  2. 类的另一个特征就是继承,protected的作用就是实现这一目的。所以:protected成员可以被派生类访问(只能在派生类的内部,也就是成员函数的形式访问,不能定义派生类对象来访问),不能被用户代码(类外)访问

有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。

  1. public继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:public, protected, private

  2. protected继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:protected,protected, private

  3. private继承:基类public成员,protected成员,private成员的访问属性在派生类中分别变成:private, private, private

11. 指针常量与常量指针

  • 常量指针指向常量的指针,也就是说指向的是常量而不是变量,指针本身可以改变(即可以指向另外一个常量),但是指向的内容不能使用指针对其进行修改
  • 指针常量指向指针,本身是常量,也就是说它的指向不能被改变,但是指向的内容可以改变。

使用的时候,记住3句话就可以了:

  • *象征着地址,const象征着内容;
  • 指针和 const 谁在前先读谁 ;
  • 谁在前面,谁就不允许改变。

常量指针举例:

#include <iostream>
using namespace std;

int main()
{
	int a = 2;
	int b = 4;
	int const *p = &a; //定义常量指针
	a = 5;			   //正常,此时*p的内容为5,但指向不变
	//*p = 3;//报错,不能改变常量指针的内容
	p = &b; //正常,可以修改指向
}

指针常量举例:

#include<iostream>
using namespace std;

int main()
{
    int a = 2;
    int b = 4;
    int * const p = &a;//定义指针常量
    *p = 3;//正常,可以修改指针常量的内容
    p = &b;//报错,不可以修改指针常量的指向
}

12. 智能指针:auto_ptr(C++11被弃用)、unique_ptr、shared_ptr和weak_ptr

  • 为什么使用智能指针

先来看一段代码:

void someFunction()
{
    Resource* ptr = new Resource; // Resource是一个类或者结构
    // 使用ptr处理
    // ...
    delete ptr;
}

如果在delete ptr之前,函数被return或者抛出异常等情况,普通指针的删除将不会被执行,从而导致内存泄漏。那么,智能指针就被排上用场了。

  • 智能指针的实现

智能指针的思想:将动态申请的资源交给一个类变量来保存,由于类变量在局部作用域,其离开后将会自动调用析构函数

  • auto_ptr

auto_ptr虽然可以自动删除指针,但是有一个问题,当两个以上指针同时指向同一个地址的时候,当指针过期的时候,程序中的将试图删除同一个对象多次,这将是不被允许的,例如下列会出现删除多次的情况:

auto_ptr<string> p1 (new string ("I reigned lonely as a cloud.")); 
auto_ptr<string> p2; 
p2 = p1; //auto_ptr不会报错.

说明以下例子:p2接管string对象的所有权后,p1的所有权将被剥夺。可防止p1和p2的析构函数试图刪同—个对象,但是,有一个问题,如果后面继续使用p1将会出现问题,因为p1不再指向有效的数据。

有以下方案解决:

  • 定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
  • 建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的构造函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略,但unique_ptr的策略更严格。
  • 创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
  • unique_ptr

unique_ptr和auto_ptr类似,也是剥夺所有权,但是unique_ptr是唯一的,编译阶段就能检测出错误

unique_ptr<string> p3 (new string ("auto"); //#4
unique_ptr<string> p4; //#5
p4 = p3; //#6,编译器检测出错误,不能指向同一个地址。

它比auto_ptr聪明的另外一个地方,就是允许临时右值(包括临时指针,std::move操作)。程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做。例如下列是允许的。

unique_ptr<string> demo(const char * s)
{
	unique_ptr<string> temp (new string (s));
	return temp;//临时变量,允许
}
unique_ptr<string> ps;
ps = demo('Uniquely special");//调用
  • shared_ptr

前面也介绍了,shared_ptr的策略是赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete

它有下列成员函数:

  • use_count : 返回引用计数的个数
  • unique : 返回是否是独占所有权( use_count 为 1)
  • swap : 交换两个 shared_ptr 对象(即交换所拥有的对象)
  • reset : 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
  • get : 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.

使用方法举例:

#include<iostream>
#include<memory>

using namespace std;
int main()
{
	string *s1 = new string("s1");

	shared_ptr<string> ps1(s1);
    cout << ps1.unique()<<endl;	//此时是唯一的,true
	shared_ptr<string> ps2;
	ps2 = ps1;
    cout << ps1.unique()<<endl;	//此时不唯一,false

	cout << ps1.use_count()<<endl;	//2
	cout<<ps2.use_count()<<endl;	//2

	string *s3 = new string("s3");
	shared_ptr<string> ps3(s3);

	cout << (ps1.get()) << endl;	//033AEB48
	cout << ps3.get() << endl;	//033B2C50
	swap(ps1, ps3);	//交换所拥有的对象
	cout << (ps1.get())<<endl;	//033B2C50
	cout << ps3.get() << endl;	//033AEB48

	cout << ps1.use_count()<<endl;	//1,因为和ps3交换了
	cout << ps2.use_count() << endl;	//2
	ps2 = ps1;//ps2和ps1又指向同一空间
	cout << ps1.use_count()<<endl;	//2
	cout << ps2.use_count() << endl;	//2
	ps1.reset();	//放弃ps1的拥有权,引用计数减少
	cout << ps1.use_count()<<endl;	//0
	cout << ps2.use_count()<<endl;	//1
}
  • weak_ptr

shared_ptr虽然已经很好用了,但是有一点shared_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

class B;	//声明
class A
{
public:
	shared_ptr<B> pb_;
	~A()
	{
		cout << "A delete\n";
	}
};

class B
{
public:
	shared_ptr<A> pa_;
	~B()
	{
		cout << "B delete\n";
	}
};

void fun()
{
	shared_ptr<B> pb(new B());
	shared_ptr<A> pa(new A());
	cout << pb.use_count() << endl;	//1
	cout << pa.use_count() << endl;	//1
	pb->pa_ = pa;
	pa->pb_ = pb;
	cout << pb.use_count() << endl;	//2
	cout << pa.use_count() << endl;	//2
}

int main()
{
	fun();
	return 0;
}

可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减1,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A、B的析构函数没有被调用)运行结果没有输出析构函数的内容,造成内存泄露。如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_,改为weak_ptr pb_ ,运行结果如下:

1
1
1
2
B delete
A delete

13. C++如何禁止对象的复制、赋值和delete的用法

禁止复制操作可以使得每个对象独一无二。

  • 方法1:将拷贝构造函数与赋值构造函数声明为private。但是对于友元函数和类成员函数来说,还是可以调用其相关的复制操作的。
  • 方法2:使用delete关键字显式指示编译器不生成函数的默认版本。(被=delete的成员函数不能被调用
  • 方法1:将复制相关的操作设置为private私有
class CPeople
{
    // ...
private:
    // 将复制相关的操作定义为私有
     CPeople(){...};
    const CPeople& operator=(const CPeople& rhis){...}
};
  • 方法2:使用delete关键字,显式指示编译器不生成函数的默认版本。
class MyClass
{
  public:
    MyClass()=default;//默认构造函数
    MyClass(const MyClass&) = delete;  //阻止拷贝
    MyClass & operator = (const MyClass&) = delete; //阻止赋值
  ......
}

delete的另一种用法:

下列如果没有void func(double data)=delete;则会使用隐式类型转换调用 void func(int data) 。

#include <cstdio>
class TestClass
{
public:
    void func(int data) { printf("data: %d\n", data); }
    void func(double data)=delete;//禁止使用double类型
};
int main(void)
{
    TestClass obj;
    obj.func(100);
    obj.func(100.0);//错误,浮点类型被禁止调用

    return 0;
}

14. override和final

  • override :它指定了子类的这个虚函数是重写的父类函数
  • final :当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或重写,编译器会报错

override举例:

class A
{
    virtual void foo();
}
class B :public A
{
    void foo(); //OK,方式1
    virtual foo(); // OK,方式2
    void foo() override; //OK,方式3
}

final举例:

class Base
{
    virtual void foo();
};
 
class A : Base
{
    void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
    void bar() final; // Error: 父类中没有 bar虚函数可以被重写或final
};

class B final : A // 指明B是不可以被继承的
{
    void foo() override; // Error: 在A中已经被final了
};
 
class C : B // Error: B is final
{
};

15. gcc与g++

  • gcc:C语言使用
  • g++:C和C++均可使用

在使用std::cout时,它是c++中的语法,gcc会编译出错,所以要用g++。

16. iota

该函数在头文件numeric中,功能为向序列中写入以val为初值的连续值序列

#include <numeric>	// std::iota
int numbers[10];
std::iota(numbers, numbers + 10, 100);//以100为初值
//numbers的值为:100 101 102 103 104 105 106 107 108 109

建立索引的时候比较好用:

        std::vector<int> pc_indices(cloud_ptr->size());
        std::iota(pc_indices.begin(), pc_indices.end(), 0);

17. limits–最大值与最小值

#include <iostream>     // std::cout
#include <limits>       // std::numeric_limits

int main () {
  std::cout << std::boolalpha;
  std::cout << "int类型的最小值: " << std::numeric_limits<int>::min() << '\n';
  std::cout << "int类型的最大值: " << std::numeric_limits<int>::max() << '\n';
  std::cout << "int是有符号: " << std::numeric_limits<int>::is_signed << '\n';
  std::cout << "int的位数: " << std::numeric_limits<int>::digits << '\n';
  std::cout << "int 是无限大小: " << std::numeric_limits<int>::has_infinity << '\n';
  return 0;
}

输出:

int类型的最小值: -2147483648
int类型的最大值: 2147483647
int是有符号: true
int的位数: 31
int 是无限大小: false

18. shared_ptr与unique_ptr对象的构造

shared_ptr和unique_ptr的头文件在#include <memory>

  • shared_ptr对象的创建
//方式1:ptr相当于一个nullptr,ptr.use_count == 0
std::shared_ptr<T> ptr;
std::shared_ptr<int> ptr (nullptr);

//方式2:new方法,ptr.use_count == 1
std::shared_ptr<T> ptr(new T);

//方式3:使用复制构造函数,ptr2.use_count++
std::shared_ptr<T> ptr2(ptr1);

//方式4:赋值
std::shared_ptr<T> a(new T());
std::shared_ptr<T> b(new T());
a = b; // 此后 a 原先所指的对象会被销毁,b 所指的对象引用计数加 1

//方式5:重置方法reset。这里也可以先ptr=nullptr
std::shared_ptr<T> ptr(new T());
ptr.reset(new T()); 重置指针,并接管新的堆空间对象

//方式6:make_shared
std::shared_ptr<int> ptr = std::make_shared<int> (10);//等价于std::shared_ptr<int> foo2 (new int(10));
auto ptr = std::make_shared<int> (20);
  • 父类与子类的智能指针
Base::Ptr infer_ptr_;//自定义的父类
infer_ptr_ = std::shared_ptr<Base>(new Derived);//指向子类对象

有文章指出,不能使用原始指针初始化多个shared_ptr,这个说法是错误的,举例如下:

#include <iostream>
#include <memory>

int main()
{
	std::shared_ptr<int> p(new int(10));
	std::shared_ptr<int> p1(p);
	std::shared_ptr<int> p2(p);

	std::cout << "use_count:\n";
	std::cout << "p1: " << p.use_count() << '\n';
	std::cout << "p2: " << p1.use_count() << '\n';
	std::cout << "p3: " << p2.use_count() << '\n';
	return 0;
}

输出:

use_count:
p1: 3
p2: 3
p3: 3
  • unique_ptr的构造
//方法1:定义空指针
  std::unique_ptr<int> ptr;
  std::unique_ptr<int> ptr (nullptr);
//方法3:new方法
std::unique_ptr<int> ptr (new int);

//方法4:赋值
std::unique_ptr<int> ptr;
ptr = std::unique_ptr<int>(new int (101));  // rvalue
  
  //方法5:reset方法
  std::unique_ptr<int> ptr;  // empty
  ptr.reset (new int);       // takes ownership of pointer
//方法6:move
std::unique_ptr<int> foo = std::unique_ptr<int>(new int (101));
std::unique_ptr<int> bar = bar = std::move(foo);                       // using std::move
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

非晚非晚

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

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

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

打赏作者

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

抵扣说明:

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

余额充值