C/C++总结

一:类的成员函数后面加 const

class A
{
public:
    void f() const
    {
        cout<<" const"<<endl;
        return var_a
    }
private:
    int var_a
};

表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。

在设计类的时候,一个原则就是对于不改变数据成员的成员函数都要在后面加 const,而对于改变数据成员的成员函数不能加 const。所以 const 关键字对成员函数的行为作了更加明确的限定:

(1)有 const 修饰的成员函数(指 const 放在函数参数表的后面,而不是在函数前面或者参数表内),只能读取数据成员,不能改变数据成员;没有 const 修饰的成员函数,对数据成员则是可读可写的。
(2)除此之外,在类的成员函数后面加 const 还有什么好处呢?那就是常量(即 const)对象(const A a,对象a即常量对象)可以调用 const 成员函数,而不能调用非const修饰的函数。

原理:

通过成员函数访问私有变量是通过this这个隐形的常量指针的,即定义的f()函数中return var_a其实质为return this->var_a, 如果此时成员函数没有加const,那么这里的this属性是常量指针(常量指针是说指针的值是const的,指针常量是说指针指向的变量的值是const的)。如果加了const则变为常指针常量。即变成了,该指针不可修改,指向的值也变成常量(即不可变)。

同样,如果在函数后面添加有final和override关键字的,final表示其派生类的成员函数不能再去重载。相反override表示其派生类的必须重载该成员函数

二:C++ 中在函数的前面加上static的作用

  1. 在一般的函数前面加上static,作用是: 
    加了static后表示该函数失去了全局可见性,只在该函数所在的文件作用域内可见 
    当函数声明为static以后,编译器在该目标编译单元内只含有该函数的入口地址,没有函数名,其它编译单元便不能通过该函数名来调用该函数,这也是对1的解析与说明

  2. 在类的成员函数前面加上static标志符: 
    成员函数是属于类的,而非对象的,也就是所有该类的对象共同拥有这一个成员函数,而不是普通的每个对象各自拥有一个成员函数

引出变量之前加static:

全局变量、静态局部变量、静态全局变量都在静态存储区分配空间,而局部变量在栈分配空间。全局变量本身就是静态存储方式,静态全局变量当然也是静态存储方式。这两者在存储方式上没有什么不同。区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源程序的其他源文件中不能使用它。静态局部变量的生存期虽然为整个工程,但是其作用仍与局部变量相同,即只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。可见,他们的生命周期相同,加上了static就是限制了其作用范围。

引出 inline之前加static:

inline函数是不能像传统的函数那样放在.c中然后在.h中给出接口在其余文件中调用的,因为inline函数其实是跟宏定义类似,不存在所谓的函数入口。这样如果inline函数在两个不同的文件中出现,也就是说一个.h被两个不同的文件包含,则会出现重名,链接失败所以static inline 的用法就能很好的解决这个问题,使用static修饰符,函数仅在文件内部可见,不会污染命名空间。可以理解为一个inline在不同的.C里面生成了不同的实例,而且名字是完全相同的。

三:const = 0 以及Virtual关键字

 virtual cv::Mat filterSingleChannel(const cv::Mat &p) const = 0;
 virtual void Show()=0

const 和 =0 是需要分开看的,他们不是一个整体
=0说明它是纯虚函数.只能让它的后代去实现这个函数的作用,自己不能实现。而且,是必须被后代实例化的。加上const 参考一

四: 析构函数作为虚函数

基类析构函数[是虚函数]: 指向派生类的基类指针在析构的时候 [普通指针,shared_ptr,unique_ptr][都会先调用派生类的析构函数,然后再调用基类的析构函数]

基类析构函数[不是虚函数]: 指向派生类的基类指针在析构的时候 [普通指针,unique_ptr][只会调用基类的析构函数,不会调用派生类的析构函数] [shared_ptr][会先调用派生类的析构函数,然后再调用基类的析构函数]

五:public、private、protected以及friend

protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问。

public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问

private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问。

friend 友元,别人是你的朋友,他可以访问我的东西。(但不是我可以访问他的东西)

  1. 友元关系不能被继承。
  2. 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
  3. 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明 
  4. 友元函数并不是类的成员函数,因此在类外定义的时候不能加上class::function name

注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数

六:STL下四种智能指针

std::auto_ptrstd::unique_ptrstd::shared_ptr和 std::weak_ptr,其中,std::auto_ptr的问题太多,已经在C++11中删除了(原因)。主要介绍其他三个:

std::unique_ptrstd::auto_ptr的替代品,其用于不能被多个实例共享的内存管理。这就是说,仅有一个实例拥有内存所有权,创建时使用make_unique函数创建unique_ptr实例。它持有对对象的独有权——两个unique_ptr不能指向一个对象,即 unique_ptr 不共享它所管理的对象。它无法复制到其他 unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL)算法。只能移动 unique_ptr,即对资源管理权限可以实现转移。这意味着,内存资源所有权可以转移到另一个 unique_ptr,并且原始 unique_ptr 不再拥有此资源:

   f2 = f1 // 非法,不允许左值赋值(即这种写法是错误的
   f2 = std::move(f1);  // 正确写法,此时f1转移到f2,f1变为nullptr

std::unique_ptr对象可以方便地管理动态内存。但是前提是该对象是建立在栈上的,千万不要使用动态分配的类对象,那么将在堆上,其行为与普通指针变得一样

std::unique_ptr还有几个常用的方法: 1. release():返回该对象所管理的指针,同时释放其所有权; 2. reset():析构其管理的内存,同时也可以传递进来一个新的指针对象; 3. swap():交换所管理的对象; 4. get():返回对象所管理的指针; 5. get_deleter():返回析构其管理指针的调用函数。例如:

//智能指针的创建  
unique_ptr<int> u_i; 	//创建空智能指针
u_i.reset(new int(3)); 	//绑定动态对象  
unique_ptr<int> u_i2(new int(4));//创建时指定动态对象
unique_ptr<T,D> u(d);	//创建空 unique_ptr,执行类型为 T 的对象,用类型为 D 的对象 d 来替代默认的删除器 delete

//所有权的变化  
int *p_i = u_i2.release();	//释放所有权  
unique_ptr<string> u_s(new string("abc"));  
unique_ptr<string> u_s2 = std::move(u_s); //所有权转移(通过移动语义),u_s所有权转移后,变成“空指针” 
u_s2.reset(u_s.release());	//所有权转移
u_s2=nullptr;//显式销毁所指对象,同时智能指针变为空指针。与u_s2.reset()等价

td::shared_ptrstd::unique_ptr类似。要创建std::shared_ptr对象,可以使用make_shared()函数

std::shared_ptrstd::unique_ptr的主要区别在于前者是使用引用计数的智能指针。引用计数的智能指针可以跟踪引用同一个真实指针对象的智能指针实例的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的内存,当最后一个引用对象离开其作用域时,才会释放这块内存。还有一个区别是std::shared_ptr不能用于管理C语言风格的动态数组,这点要注意

std::weak_ptr是由于相互引用引起的内存泄漏问题

七:share_ptr实现多态以及注意事项

虚函数,抽象类,覆盖,模板都可以实现多态。同内置指针一样,智能指针类也支持派生类向基类的类型转换,这意味着我们可以将一个派生类对象的指针存储在一个基类的智能指针内。

class Person{
public:
    string name;
    Person(){}
    Person(const char* _name){name=_name;}
    virtual void show(){
        cout<<name<<" show in Person"<<endl;
    }
    virtual ~Person(){cout<<name<<" delete in Person\n";}
};

class PersonA:public Person{
public:
    PersonA(){}
    PersonA(const char* _name){name=_name;}
    void show(){
        cout<<name<<" show in PersonA"<<endl;
    }
    virtual ~PersonA(){cout<<name<<" delete in PersonA\n";}
};
class PersonB:public Person{
public:
    PersonB(){}
    PersonB(const char* _name){name=_name;}
    void show(){
        cout<<name<<" show in PersonB"<<endl;
    }
    virtual ~PersonB(){cout<<name<<" delete in PersonB\n";}
};
int main(){
    Person person("小明");
    person.show();
    PersonB person2("小李");
    person2.show();
    PersonA person3("小张");
    person3.show();
    return 0;
}

这是以上程序打印出的结果,是普通方式调用各类的函数,可以看出在程序执行完后,都相应的调用了析构函数和基类的析构函数。

一般内置指针进行调用:

int main(){
    Person* person = new Person("小明");
    person->show();
    person = new PersonA("小李");
    person->show();
    person = new PersonB("小张");
    person->show();
   return 0;
}

1. 可以看出,使用new生成的对象,在程序结束的时候也没有调用析构函数,这个和C++中的内存保存地址有关,之前的例子,对象的地址保存在栈中,这个内存不需要我们手动分配,当然删除这个内存也不需要我们,所以编译器会在程序结束的时候,自动调用析构函数,把栈中的对象清空。
2. 但是,这里我们使用的是new,申请的对象的地址是保存在堆中,需要我们手动申请空间,当然,删除这个空间也需要我们手动删除。但是我们没有在程序的最后进行删除对象,所以最后没有调用析构函数,这样很容易造成内存泄漏。但是,一个对象何时删除,这是一个比较麻烦的事情。
3. 所以我们可以使用智能指针累,来帮我们手动删除对象。

智能指针:

int main(){
    shared_ptr<Person> person = make_shared<Person>("小明");
    person->show();
    person = make_shared<PersonA>("小李");
    person->show();
    person = make_shared<PersonB>("小张");
    person->show();
    return 0;
}

è¿éåå¾çæè¿°

现在我们发现,当智能指针person在指向其他对象的时候,之前的对象就调用析构函数,删除了指针对象的地址。同时在程序结束的时候,再次析构了当前对象。我们使用智能指针,我们可以不用考虑什么时候删除对象,会自动帮我们删除对象。

但是智能指针也有两种弊端。

    1. 采用make_shared创建的智能指针虽好, 但也存在一些问题, 比如, 当我想要创建的对象没有公有的构造函数时, make_shared 就无法使用了

    2. make_shared 只分配一次内存, 这看起来很好. 减少了内存分配的开销. 问题来了, weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 而因此连带着保持了对象分配的内存, 只有最后一个 weak_ptr 离开作用域时, 内存才会被释放. 原本强引用减为 0 时就可以释放的内存, 现在变为了强引用, 若引用都减为 0 时才能释放, 意外的延迟了内存释放的时间. 这对于内存要求高的场景来说, 是一个需要注意的问题

其真正的原理采用引用计数的方式实现什么时候应当删除对应。智能指针是指针!指针所指向的对象有一个引用次数的属性,当引用次数为0时,该对象就会被析构。由此引入了相互引用问题,这在iOS上的OC语言中经常遇到的。

八:int8_t、int16_t、int32_t、int64_t、uint8_t、size_t、ssize_t区别

int_t类:int_t 为一个结构的标注,可以理解为type/typedef的缩写,表示它是通过typedef定义的,而不是一种新的数据类型。因为跨平台,不同的平台会有不同的字长,所以利用预编译和typedef可以最有效的维护代码

 int8_t      : typedef signed char;
 uint8_t    : typedef unsigned char;
 int16_t    : typedef signed short ;
 uint16_t  : typedef unsigned short ;
 int32_t    : typedef signed int;
 uint32_t  : typedef unsigned int;
 int64_t    : typedef signed  long long;
 uint64_t  : typedef unsigned long long;

size_t与ssize_t

size_t主要用于计数,如sizeof函数返回值类型即为size_t。在不同位的机器中所占的位数也不同,size_t是无符号数,ssize_t是有符号数。

在32位机器中定义为:typedef  unsigned int size_t; (4个字节)
在64位机器中定义为:typedef  unsigned long size_t;(8个字节)
此外,int 无论在32位还是64位机器中,都是4个字节, 且带符号,可见size_t与int 的区别之处。

九:结构体内存对齐

  • 一般来说,对于需要X字节的原始数据类型,地址必须是X的倍数.
  • struct的起始地址决于其成员变量中的数据类型的sizeof()最大值作为对齐条件。
  • 编译器在编译阶段对struct中未使用的内存空间执行填充操作,以确保字段对齐。
  • char类型不需要对齐,可自由分配任意可用的1字节尺寸的内存空间之中.
    struct foo{
          char c;
          int [2];
          double d;
    }

  • 根据规则1和2:由于这个结构体的对齐条件是8字节,因为它是struct成员变量中对齐条件最大是8个字节,所以这个结构体的起始地址必须是8的倍数,从低位r地址算起当然是地址8的倍数,例如160.

  • 根据规则4:由于char类型是没有任何对齐条件的限制,并且在结构体中第一个声明的变量,所以它就落在结构体的起始地址,也就是地址160的位置.

  • 根据规则1,由于int数组类型的每个元素占用4个字节,那么int类型的起始地址以4的倍数开始的那么就自然落到了164这个地址,紧挨着的i[2]元素自然落在168这个地址.

  • 成员变量c和int[0]之间有还有未使用的内存空间,根据规则3,会填充未使用的字节.

  • 根据规则1,Double类型的成员以8的倍数对齐,自然落在自struct起始地址起第16个字节的位置,即地址176的地方算起占用8个字节表示成员变量d的数据.

  • 根据规则3,另外编译器会填充成员变量d和数组元素int[1]之间未使用的内存位置

内存节省:

struct{
    double   d;
    int      i[2];
    char     c;
} *b;

相比上面的例子,内存改变为:

在struct内部,将成员变量按照其类型的sizeof()值由大到小,依存声明的话能够不仅可以最大限度地减少编译器填充未使用内存块的操作,而且填充的内存块出现的次数越少,那么CPU每次从对应内存块中加载数据到寄存器中执行shift运算的次数也会相应地减少。同时能够兼顾CPU对对齐内存的访问.

结构体数组内存分布:

结构体的数组是无法满足上面的所讲的节省内存的特性的。

我们已经知道结构体b的对齐条件是8的倍数,由于结构体b的占据17个字节的内存空间,因此和它相关的数组中的每个元素占用内存空间必须要达到24个字节,才能达成每个元素的对齐条件是8的倍数, 因此每个结构体元素,编译器还需要为每个元素填充7个字节

十:内存管理提示:

new和delete运算符的特性与处理C中的内存分配的函数集(即malloc/realloc等)相反,C ++中的内存分配由运算符new和delete这两个操作符去处理。 malloc和new之间的重要区别是

  1. 函数malloc不理会分配的内存用来干什么的。 相反,new需要指定类型; sizeof表达式由编译器隐式处理。 因此,使用new是类型安全的。

在C中,如果对于char指针分配一个内存宽,那情况复杂得多了,首先你需要预估加载到堆空间的字符个数。是否需要在现已分配的基础上扩容?扩充多少内存空间?

  1. malloc分配的内存是由calloc初始化的,将分配的字符初始化为可配置的初始值。 当对象可用时,这不是很有用。 当new的操作符知道分配的实体的类型时,它可以(并将)调用分配的类类型对象的构造函数。 该构造函数也可以提供参数。
  2. 在C中执行内存分配操作 ,必须检查所有C分配函数是否有NULL返回。 而C++中,使用new时不需要。 实际上,当遇到失败的内存分配时,new的行为可以通过使用new_handler来配置

释放和删除之间存在类似的关系:删除可确保在释放对象时自动调用其析构函数

十一:FILE文件流的使用

主要的操作函数fopen、fseek、fread、fclose

FILE * fopen(const char *path,cost char *mode)

作用:打开一个文件,返回指向该文件的指针

参数说明:第一个参数为欲打开文件的文件路径及文件名,第二个参数表示对文件的打开方式

注:mode有以下值:

r:只读方式打开,文件必须存在

r+:可读写,必须存在

rb+:打开二进制文件,可以读写

rt+:打开文本文件,可读写

w:只写,文件存在则文件长度清0,文件不存在则建立该文件

w+:可读写,文件存在则文件长度清0,文件不存在则建立该文件

a:附加方式打开只写,不存在建立该文件,存在写入的数据加到文件尾,EOF符保留

a+:附加方式打开可读写,不存在建立该文件,存在写入的数据加到文件尾,EOF符不保留

wb:打开二进制文件,只写

wb+:打开或建立二进制文件,可读写

wt+:打开或建立文本文件,可读写

at+:打开文本文件,可读写,写的数据加在文本末尾

ab+:打开二进制文件,可读写,写的数据加在文件末尾

由mode字符可知,上述如r、w、a在其后都可以加一个b,表示以二进制形式打开文件

注意:在fopen操作后要进行判断,是否文件打开,文件真正打开了才能进行后面的读或写操作,如有错误要进行错误处理

size_t fread(void* buff,size_t size,size_t count,FILE* stream)

作用:从文件中读入数据到指定的地址中

第一个参数为接收数据的指针(buff),也即数据存储的地址

第二个参数为单个元素的大小,即由指针写入地址的数据大小,注意单位是字节

第三个参数为元素个数,即要读取的数据大小为size的元素个素

第四个参数为提供数据的文件指针,该指针指向文件内部数据

返回值:读取的总数据元素个数

size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)

第一个参数,这是指向要被写入的元素数组的指针

第二个参数,这是要被写入的每个元素的大小,以字节为单位。

第三个参数,这是元素的个数,每个元素的大小为 size 字节。

第四个参数,这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。

可以对比fread函数的参数的解释,相互理解一下参数的意义

int fseek(FILE *stream,long offset,int fromewhere)

作用:重定位文件内部的指针

参数:第一个为文件指针,第二个是指针的偏移量,第三个是指针偏移起始位置

返回值:重定位成功返回0,否则返回非零值,如果执行成功,stream将指向以fromwhere为基准,偏移offset个字节的位置

需要注意的是该函数不是重定位文件指针,而是重定位文件内部的指针,让指向文件内部数据的指针移到文件中我们感兴趣的数据上,重定位主要是这个目的。

说明:执行成功,则stream指向以fromwhere为基准,偏移offset个字节的位置。执行失败(比方说offset偏移的位置超出了文件大小),则保留原来stream的位置不变

int fclose(FILE *stream)

功能:关闭一个文件流,使用fclose就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区

熟练使用以上四个函数可以从文件中获取对我们有用的数据型,前提对于文件格式很了解,比如,对于一个DIB位图文件,就可以读取出他的文件中的头信息和像素点信息。

int ftell(FILE *__stream)

返回文件当前指针指向位置,与fseek配合可以算出文件元素数据总数

void rewind (FILE *__stream)

将文件内部的位置指针重新指向一个流(数据流/文件)的开头,rewind函数作用等同于 (void)fseek(stream, 0L, SEEK_SET)

以读取yuv420格式的视频文件为例说明具体用法:

    FILE* inF = fopen(input, "rb");
    FILE* outF = fopen(Output, "wb");
    if(inF==NULL) {
        cout<<"can not find input file";return -1;
    }
    if(outF==NULL) {
        cout<<"can not find outputput file";return -1;
    }
    // 计算帧数
    fseek(input, 0, SEEK_END);
    long FrameNum = ftell(input);
    FrameNum = FrameNum / (width * height * 3 / 2);
    rewind(fileHandle);
    // 做了简化,只处理一帧
    size_t res=fread(yuv_data->y, sizeof(unsigned char), width*height, inF);
    fwrite(out_data->y, sizeof(unsigned char), width*height, outF);
    
    goto exit:

exit:

    fclose(inF);
    fclose(outF);
 

其中部分参数解释:SEEK_SET:表示文件的相对起始位置;SEEK_CUR:表示文件的相对当前位置;SEEK_END:表示文件的相对结束位置

十二:函数指针

一般三种写法,比较常用的是第三种

// 第一种
#include <iostream>
using namespace std;
void test(void (func)(int)) // 参数是另外一个函数
{
    func(100);
}

void fn(int data){
    cout << "data is " << data << "\n";
}

int main(){
    test(fn); // 把fn作为参数传递给test()
    return 0;
}

第二种使用typedef

#include <iostream>
using namespace std;
typedef void func(int); // 函数类型定义
void test(func * pFunc) // 注意*
{
    pFunc(100);
}
void fn(int data){
    cout << "data is " << data << "\n";
}
int main(){
    test(fn);
    return 0;
}

第三种:

#include <iostream>
using namespace std;
typedef void (*func)(int); // 函数类型定义,多了圆括号和*
void test(func pFunc) // 没有*
{
    pFunc(100);
}
void fn(int data){
    cout << "data is " << data << "\n";
}
int main(){
    test(fn); // 调用方式和第2
    return 0;
}

指针为形参时的注意事项

在区分形参和实参时经常举得例子为:

void Swap(int x, int y){
	int tmp;
	tmp = x;
	x = y;
	y = tmp;
}

这里并没有办法将x,y的值进行互转,因为函数是值传递。这里当参数为指针类型时也要特别注意这一点。比如:

class Test{
public:
	void setPtr(int *ptr){
		m_ptr = ptr;
	}
	bool getPtr(int * ptr){
	if (m_ptr == nullptr){
		return false;
	}else{
		ptr = m_ptr;
		return true;
	}
}	
private:
	int * m_ptr;
};
int main(void){
	Test test;
	int data = 100;
	test.setPtr(&data);
	int *retPtr = nullptr;
	if (test.getPtr(retPtr) == true){
		std::cout << *retPtr << "\n";
	}
	return 0;
}

这里设置了test对象的私有变量,但是打印出来结果为空。主要原理就是值传递原因。在将retPtr传给函数的参数时,实际上执行的是 int *ptr=retPtr; 这里只是将retPtr初始化给了ptr。然后函数内部将私有变量的值(该值是一个指针,也就是地址值)付给了ptr,他的改变不影响retPtr。在了解该过程中,首先解释一下*和&在不同的位置表示的含义:

&在变量定义区,表示引用 int &x ;
&在变量操作区,表示取地址符 int x=10, *p=&x ;
*在变量定义区,表示指针 int *x ;
*在变量操作区,表示解引用 int *x ;cout<<*x ;

test.getPtr(retPtr)时,执行getPtr(int * ptr)函数,这里只是将ptr和retPtr的指向的地址都置为NULL。getPtr函数内部是对ptr指向的地址进行改变,无论怎么改变,都不影响retPtr指向的地址。

因为是改变的地址,而不是位置上的值。所以,如果想要正确的改变,就需要将地址作为一个指针,即地址的地址。

正确方法:

bool getPtr(int ** pptr)
{
	if (m_ptr == nullptr){
		return false;
	}else{
		(*pptr) = m_ptr; // key
		return true;
	}
}
int main(void){
	Test test;
	int data = 100;
	test.setPtr(&data);
	int *retPtr = nullptr;
	if (test.getPtr(&retPtr) == true) // 要取retPtr的地址传过去
	{
		std::cout << *retPtr << "\n";
	}
	return 0;
}

使用二级指针。代码中最关键的是(*pptr) = m_ptr;,也是容易搞错的地方,这是对二级指针解引用然后赋值,千万别写成pptr = &m_ptr;,这样又变成前面的传值问题,无法拿到正确的指针值。使用了二级指针,形参pptr初始化为一个空指针。在函数内部,直接将私有变量的值(也是一个地址)传给了pptr,pptr的值不再为空(他的值是一个地址,该值就是私有变量m_ptr的值)。所以在使用时,std::cout << *retPtr << "\n";,这里有*,即*retPtr——取值符号。而使用一级指针,我们并没有为retPtr的值做些什么。一直在改变的是形参ptr。而在二级指针里面,函数内部是做了取值操作来改变的其值。 int *pptr=&retPtr,表明pptr和retPtr是同一份地址。所以改变其中一个地址的值,就是改变另一个。

同理,也可以使用引用得到正确答案。因为引用就是取址操作。

bool getPtr(int *& ptr){
	if (m_ptr == nullptr){
		return false;
	}else{
		ptr = m_ptr;
		return true;
	}
}
// 可以使用typedef代替
typedef int *  newType;
bool getPtr(newType& ptr){
	if (m_ptr == nullptr){
		return false;
	}else{
		ptr = m_ptr;
		return true;
	}
}
int main(void){
	Test test;
	int data = 100;
	test.setPtr(&data);
	int *retPtr = nullptr;
	if (test.getPtr(retPtr) == true) {
		std::cout << *retPtr << "\n";
	}
    return 0;
}

总结指针的指针(使用二级指针)的作用

如果你希望在一个函数的参数中改变一个指针的值,你就只能传这个指针的指针给这个函数

关键字extern和explicit

extern关键字可以用来声明变量和函数作为外部变量或者函数供其它文件使用,比如在某个test.h中有两个变量声明a,b。相应的test.c有其定义。我们在main.c中想要使用a,b就必须有#include"test.h".但是有了extern关键字后就不再需要:

#include <stdio.h>
extern int a;
extern int b;
int main(void){
 printf("a = %d , b = %d\n", a, b);
 return 0;
}

同理,也适合函数的使用

extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

#ifndef __TEST_H   /*防止该头文件被重复引用*/
#define __TEST_H

#ifdef __cplusplus             
extern "C"{

#endif

/*…*/

#ifdef __cplusplus
}
#endif

#endif /*end of __TEST_H*/

__cplusplus为C++定义的一个宏,此时extern "C"后的部分的代码按C语言的格式进行编译,而不是C++.

引出:我们一般将.h和.c分开,并且一个工程有好多个h和c文件。但在最终编译的时候,都会把头文件中的声明和C文件中的定义放在一个文件里面。为了防止重复加载h文件里面的声明函数,使用了#ifndef __TEST_H  #define __TEST_H。

explicit:

 C++中的explicit关键字只能用于修饰只有一个参数的类构造函数, 它的作用是表明该构造函数是显示的, 而非隐式的, 跟它相对应的另一个关键字是implicit, 意思是隐藏的,类构造函数默认情况下即声明为implicit(隐式)。

class Demo{
     public:
      Demo();                     /* 构造函数1 */
      Demo(double a);              /* 示例代码2 */
      Demo(int a,double b);           /* 示例代码3 */
      Demo(int a,int b=10,double c=1.6);  /* 示例代码4 */
      ~Demo();
      void Func(void);
      private:
      int value1;
      int value2; };

只有一个参数的构造函数,或者构造函数有n个参数,但有n-1个参数提供了默认值,这样的情况才能进行类型转换。因此在上面进行了类demo的描述后,对应的四种构造函数:

构造函数1没有参数,无法进行类型转换!

构造函数2有一个参数,可以进行类型转换,如:Demo test; test = 12.2;这样的调用就相当于把12.2隐式转换为Demo类型。

构造函数3有两个参数,且无默认值,故无法使用类型转换!

构造函数4有3个参数,其中两个参数有默认值,故可以进行隐式转换,如:Demo test;test = 10; 

在使用了explicit关键字后,即在只有一个构造函数的前面加上explicit:

explicit Demo(double a);

在继续用Demo test;test = 12.2;是无效的。但是可以进行显示类型的转换

Demo test;

test = Demo(12.2); 或者

test = (Demo)12.2

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值