C++学习笔记(初始化列表,左右值,链接库的显式调用和隐式调用,预处理指令,内联函数,多态、虚函数,野指针、悬空指针、智能指针)

代码段一:
class pl
{
    public:
        pl(double a, double b,double c): a(a),b(b),c(c){}
    private:
        double a,b,c;
};

看到这里的a(a),b(b),c(c){}的时候有点懵,这里显然不是在调用构造函数。通过查看定义,发现a(a)中,前一个a指向下面定义的变量a,后面的a指向前面的形参a
在这里插入图片描述在这里插入图片描述
于是乎我就猜测,这段代码的功能实际是把形参初始化赋值给pl的成员变量a,b,c。于是乎,我写了下方这样一段代码:

#include <iostream>
using namespace std;

class pl
{
    public:
        pl(double a, double b,double c): a(a),b(b),c(c){}
        float get_a(){return this->a;};
        float get_b(){return this->b;};
        float get_c(){return this->c;};
    private:
        double a,b,c;
};

int main()
{
    pl *p1;
    p1 = new pl(1.1,2.2,3.2);
    cout<<"a="<<p1->get_a()<<"   "<<"b="<<p1->get_b()<<"   "<<"c="<<p1->get_c()<<endl;
    delete p1;
}

运行结果:
在这里插入图片描述
果然,它的作用与想像的一样。

经过一番搜索,我在:https://blog.youkuaiyun.com/qq_33268859/article/details/89001718https://blog.youkuaiyun.com/qq_33757398/article/details/81331918 中找到了它含义。

这段跟在构造函数后面的代码属于初始化列表,在类初始化构造函数时,对类的成员变量进行初始化。可以有如下写法:

#include <iostream>
using namespace std;

class pl
{
    public:
        pl(): a("1"),b("2"),c("3"){}
        string a,b,c;
};

int main(){
    pl *p1;
    p1 = new pl(); //执行构造函数才有效
    cout<<p1->a<<endl<<p1->b<<endl<<p1->c;
    delete p1;
}

运行结果:
在这里插入图片描述

1、左值、右值:

左值是可以用来获取内存地址的变量,右值是存储的内容的值。

const int a=1

可以通过&a来获取其地址,a就是左值,1是其内容的值,是临时的,算右值。

2、链接库的显式调用和隐式调用:

资料来自:https://blog.youkuaiyun.com/xiamentingtao/article/details/51052918

  • 显式

显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用LoadLibraryMFC提供的AfxLoadLibrary显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibraryMFC提供的AfxFreeLibrary释放动态链接库。下面是通过显式链接调用DLL中的Max函数的例子。

#include <windows.h>
#include <cstdio>
void main(void)
{
typedef int(*pMax)(int a,int b);
typedef int(*pMin)(int a,int b);
HINSTANCE hDLL;
PMax Max
HDLL=LoadLibrary("MyDll.dll");//加载动态链接库MyDll.dll文件;
Max=(pMax)GetProcAddress(hDLL,"Max");
A=Max(5,8);
Printf("比较的结果为%d\n",a);
FreeLibrary(hDLL);//卸载MyDll.dll文件;
}

在上例中使用类型定义关键字typedef,定义指向和DLL中相同的函数原型指针,然后通过LoadLibray()DLL加载到当前的应用程序中并返回当前DLL文件的句柄,然后通过GetProcAddress()函数获取导入到应用程序中的函数指针,函数调用完毕后,使用FreeLibrary()卸载DLL文件。在编译程序之前,首先要将DLL文件拷贝到工程所在的目录或Windows系统目录下。

使用显式链接应用程序编译时不需要使用相应的Lib文件。另外,使用GetProcAddress()函数时,可以利用MAKEINTRESOURCE()函数直接使用DLL中函数出现的顺序号,如将GetProcAddress(hDLL,"Min")改为GetProcAddress(hDLL, MAKEINTRESOURCE(2))(函数Min()DLL中的顺序号是2),这样调用DLL中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。

  • 隐式

隐式链接是在程序开始执行时就将DLL文件加载到应用程序当中。实现隐式链接很容易,只要将导入函数关键字_declspec(dllimport)函数名等写到应用程序相应的头文件中就可以了。下面的例子通过隐式链接调用MyDll.dll库中的Min函数。首先生成一个项目为TestDll,在DllTest.hDllTest.cpp文件中分别输入如下代码:

//Dlltest.h
 
#include"MyDll.h"
#pragma comment(lib,"MyDll.lib")    //隐式链接MyDll
extern "C"_declspec(dllimport) int Max(int a,int b);
extern "C"_declspec(dllimport) int Min(int a,int b);
//TestDll.cpp
#include"Dlltest.h"
void main()
{int a;
a=min(8,10)
printf("比较的结果为%d\n",a);
}

在创建DllTest.exe文件之前,要先将MyDll.dllMyDll.lib拷贝到当前工程所在的目录下面,也可以拷贝到windowsSystem目录下。如果DLL使用的是def文件,要删除TestDll.h文件中关键字extern "C"TestDll.h文件中的关键字Progam commit是要Visual C++的编译器在link时,链接到MyDll.lib文件,当然,开发人员也可以不使用#pragma comment(lib,"MyDll.lib")语句,而直接在工程的Setting->Link页的Object/Moduls栏填入MyDll.lib即可。

3、预处理指令:

(来自:https://www.cnblogs.com/lidabo/archive/2012/08/27/2658914.html

作用:

把通过预处理的内建功能对一个资源进行等价替换,最常见的预处理有: 文件包含,条件编译、布局控制和宏替换4种。

文件包含:

#include 是一种最为常见的预处理,主要是做为文件的引用组合源程序正文。

条件编译:

#if,#ifndef,#ifdef,#endif,#undef等也是比较常见的预处理,主要是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到版本控制、防止对文件重复包含的功能。

布局控制:

#progma,这也是我们应用预处理的一个重要方面,主要功能是为编译程序提供非常规的控制流信息。

宏替换:

#define,这是最常见的用法,它可以定义符号常量、函数功能、重新命名、字符串的拼接等各种功能。

常用的预处理指令:
#define         宏定义
#undef          取消宏
#include        文本包含
#ifdef          如果宏被定义就进行编译
#ifndef         如果宏未被定义就进行编译
#endif          结束编译块的控制
#if             表达式非零就对代码进行编译
#else           作为其他预处理的剩余选项进行编译
#elif           这是一种#else和#if的组合选项
#line           改变当前的行数和文件名称
#error          输出一个错误信息
#pragma         为编译程序提供非常规的控制流信息

#define #undef #include是最常用的,我就不记录了。

(1)#ifdef、 #if、#line、#error:

这几项上面解释里面已经写得很明显了,这几个的用法就用代码写一下吧。

  • #ifdef#error
#include <iostream>
using namespace std;
#define ll long long
#ifndef ll                 //如果宏被定义就进行编译
    #error ll not defed       //当ll未被定义时,此处编译器会红线提示错误,错误信息 ll not defed
#endif                    //结束编译块的控制

int main() {
    return 0;
}

未定义时:
在这里插入图片描述

  • #line
int main() {
    #line 11 "aa"     //指定当前11行,文件名aa
    cout<<__LINE__<<"  "<<__FILE__<<endl;   //这里endl表示输出结果换行,和程序的行数无关
    cout<<__LINE__<<"  "<<__FILE__;
    return 0;
}

输出结果:
在这里插入图片描述

  • #if

#ifif不同之处在于#if是预编译过程中的,如果不符合#if内容的分支的代码,就不会编译,而if则是每一句都会编译,只是执不执行的问题。输入如下代码,点击build

#include <iostream>
using namespace std;
#define i 3;
#if defined i    //已定义
    #pragma message("defined!")   //显示信息
#else     
    #pragma message("not defined!")
#endif

int main() {
    return 0;
}

在这里插入图片描述

(2)#pragma用法:

(参考:https://blog.youkuaiyun.com/qq_27870421/article/details/99305970

  • #pragma message
    编译时输出信息。
#include <iostream>
#pragma message("编译信息!")
using namespace std;
int main()
{
    return 0;
}

在这里插入图片描述

  • #pragma code_seg
    该指令用来指定函数在 .obj文件中存放的节 ,观察OBJ文件可以使用VC自带的dumpbin命令行程序 ,函数在.obj文件中默认的存放节为 .text节,如果code_seg没有带参数的话 ,则函数存放在.text节中。
#pragma code_seg( [ [ { push | pop}, ] [ identifier, ] ] [ "segment-name" [, "segment-class" ] )
  • #pragma once
    这是一个比较常用的C/C++杂注,只要在头文件的最开始加入这条杂注,就能够保证头文件只被编译一次。

  • #pragma hdrstop
    表示预编译头文件到此为止,后面的头文件不进行预编译。

如:

#include <iostream>
using namespace std;
#pragma message("编译信息!")
#pragma hdrstop
int main()
{
    return 0;
}

如果我把#pragma message("编译信息!")写在#pragma hdrstop编译的时候就不会输出编译信息

  • #pragma warning
    该指令允许有选择性的修改编译器的警告消息的行为,指令格式如下:
#pragma warning( warning-specifier : warning-number-list [; warning-specifier : warning-number-list...]
#pragma warning(push[,n ] )
#pragma warning(pop )
  • pragma comment
    该指令将一个注释记录放入一个对象文件或可执行文件中。

格式:

#pragma comment( "comment-type" [, commentstring] )

comment-type(注释类型):可以指定为五种预定义的标识符的其中一种,五种预定义的标识符为:

compiler:将编译器的版本号和名称放入目标文件中,本条注释记录将被编译器忽略。如果你为该记录类型提供了commentstring参数,编译器将会产生一个警告。例如:#pragma comment( compiler )

exestr: 将commentstring参数放入目标文件中,在链接的时候这个字符串将被放入到可执行文件中,当操作系统加载可执行文件的时候 ,该参数字符串不会被加载到内存中。但是,该字符串可以被dumpbin之类的程序查找出并打印出来,你可以用这个标识符将版本号码之类的信息嵌入到可执行文件中!

lib: 这是一个非常常用的关键字,用来将一个库文件链接到目标文件中常用的lib关键字,可以帮我们连入一个库文件。

user: 将一般的注释信息放入目标文件中commentstring参数包含注释的文本信息,这个注释记录将被链接器忽略。

linker:指定一个连接选项,这样就不用在命令行输入或者在开发环境中设置了。

如:

#include "op.h"
#pragma comment(lib,“op.lib”)

表示链接到op.lib这个库,就无需在project setting中进行设置了。

  • #pragma pack
    该指令指定结构和联合成员的紧凑对齐。编译器为了优化计算效率,原本1字节的可能就会被划为占用4字节,紧凑对齐后虽然可能会导致效率变低,但占用的内存减少了。

#pragma pack(1)表示设置结构体的边界对齐为1个字节,也就是所有数据在内存中是连续存储。

具体的例子代码可以参考上面的链接。

pack一些方法:

#pragma pack (n)             作用:C编译器将按照n个字节对齐。
#pragma pack ()              作用:取消自定义字节对齐方式。

#pragma  pack (push,1)     	 作用:是指把原来对齐方式设置压栈,并设新的对齐方式设置为一个字节对齐
#pragma pack(pop)            作用:恢复对齐状态
  • #pragma resource
    网上这一项资料少的可怜,能找到的就是下面这一句。

#pragma resource "*.dfm"表示把*.dfm文件中的资源加入工程。
*.dfm中包括窗体外观的定义。

(3)补充:

预定义标识符:(使用时参照上方)
__FILE__: 正在编译的文件的名字。
__LINE__: 正在编译的文件的行号。
__DATE__: 编译时刻的日期字符串。
__TIME__: 编译时刻的时间字符串。

#include <>:表示从标准库路径中查找。
#include "":表示从当前项目路径中查找。

4、内联函数:

当编译器编译时,编译器会把该内联函数的内容放到调用了这个函数的地方,以此提高效率。

使用时,在函数前加上inline

#include <iostream>
using namespace std;

inline void swap(int &x, int &y)
{
    int i = x;
    x = y;
    y = i;
}

int main( )
{
    int a = 1,b=10;
    swap(a,b);
    cout <<a<<"   "<<b<< endl;
    return 0;
}

在内联函数内不允许用循环语句和开关语句。在网上查找了一下大概是编译器只会将足够简单的内联函数认为是内联函数,而for循环和switch太过复杂,编译器不会将它认作是内联函数,编译阶段不会将它们置换到函数调用处。

5、多态、虚函数:

C++的多态表示在派生类中重写基类中的虚函数函数。在调用时,C++的动态绑定会先确定当前指向的派生类的对象,然后执行对应的虚函数。

有点类似重载和模板特化,不过重载和模板特化是通过参数类型来判断执行哪个函数,而使用多态特性则指向的是当前的派生类的对象。

为了使得派生类的成员函数可以被重写,函数声明前面需要加上virtual,表示它是一个虚函数,它允许在子类中被重写。

#include <iostream>
using namespace std;

class pl1
{
public:
    pl1(){};
    virtual char* figure()  //虚函数
    {
        return "我是pl1";
    }
};

class pl2:public pl1
{
public:
    pl2(){};
    virtual char* figure()   //虚函数
    //char* figure()    //此处也可以不声明为虚函数,只是这样就无法在pl3的派生类中重写了
    {
        return "我是pl2";
    }
};

class pl3: public pl1
{
public:
    pl3(){};
    virtual char* figure()  //虚函数
    //char* figure()    //此处也可以不声明为虚函数,只是这样就无法在pl3的派生类中重写了
    {
        return "我是pl3";
    }
};

int main(){
    pl1 *p1[3];
    p1[0] = new pl1();
    p1[1] = new pl2();
    p1[2] = new pl3();
    cout<<p1[0]->figure()<<endl   //指向基类
        <<p1[1]->figure()<<endl   //指向派生类 pl2
        <<p1[2]->figure();  //指向派生类 pl3
    delete p1[0];
    delete p1[1];
    delete p1[2];
}

运行结果:
在这里插入图片描述

如果pl1的函数不声明为虚函数,就会输出如下:
在这里插入图片描述

关于其他的一些虚函数相关内容,可以参考:https://www.cnblogs.com/cxq0017/p/6074247.html

6、野指针、悬空指针、智能指针:
(1)野指针:

(参考自 百度百科)
野指针就是指向的位置是不可知的指针。野指针的成因主要是未定义指针指向:

  • 指针变量在定义时未初始化:
    任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。
    指针指向了一个地址是不确定的变量,此时去引用就是去访问了一个不确定的地址,结果是不可知的,或许那个地址的值是空的,又或许是之前已经定义的某个变量。
add *ap;    //创建指针却既不初始化,也不置为NULL

指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。如果没有初始化,编译器会报错 'point' may be uninitializedin the function

(2)悬空指针:

一个指针的指向对象已被删除或地址被回收,那么就成了悬空指针。

//代码来自百度百科
int main(){
char*dp = NULL;
for(i=0;i<1;i++) {
char c;
dp =&c;
}
/* 注意c的声明周期 */
/* dp 此时为悬空指针 */
}
这里for循环只是说明c是局部变量
void f(){
char*dp;
/* dp 未初始化,是野指针 */
}
(3)智能指针:

以下参考自:https://blog.youkuaiyun.com/K346K346/article/details/81478223

动态内存管理时,经常会发生忘记释放内存导致内存泄漏,或是删除了正在引用某处内存的指针,导致程序非法访问。智能指针能够很好的避免这个问题。

智能指针是一个类,当程序运行到智能指针的作用域之外时,智能指针会自动调用其析构函数,析构函数会自动释放资源。可以理解为,某一段的程序运行结束后,这一段程序作用域中的智能指针会自动释放。

添加库文件#include<memory>,就可以使用智能指针了。stl中的智能指针:

    auto_ptr<int> p1(new int(1));  
    unique_ptr<int> p2(new int(2));
    shared_ptr<int> p3(new int(3));
    weak_ptr<string> p4;

除了这些,boost库中还有几个智能指针,我这里就不做记录了,等以后学习boost库时再去研究。

auto_ptrC++11之前的智能指针,在C++11之后被弃用,因为auto_ptr有时在所有权转交后,访问原有对象可能会出现程序异常,但看功能,auto_ptrshared_ptr相似。

智能指针中new出来的对象会交给智能指针托管,使用结束后会自动释放,因此不需要手动delete这个new出来的对象。

auto_ptr可以直接进行指针赋值,但某些情况下会导致程序出错。unique_ptr类型指针具有独有权,无法进行直接的指针赋值,但可以转移其所有权。

auto_ptr<int> ap1(new int(1));  
auto_ptr<int> ap2(ap1);  //可以拷贝
auto_ptr<int> ap3;
ap3 = ap2;     //可以指针复制。

unique_ptr<int> up1(new int(2));
unique_ptr<int> up2(up1);  //报错,无法拷贝
unique_ptr<int> up3;
up3 = up1;         //报错,无法复制

up3 = unique_ptr<int>(new int(2));  //可以运行
unique_ptr<int> up4 = move(up1);   //可以运行,此时将up1的所有权转交给up4,up1成为无效指针

unique_ptr的基本使用:

// 智能指针的创建
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()等价

shared_ptr 是一个标准的共享所有权的智能指针,允许多个指针指向同一个对象。shared_ptr 利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个 shared_ptr 共同管理同一个对象。

shared_ptr解决了unique_ptr的所有权独占问题,但也带来了额外的开销:

  • shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针;
  • 时间上的开销主要在初始化和拷贝操作上, *-> 操作符重载的开销跟 auto_ptr 是一样;
  • 开销并不是我们不使用 shared_ptr 的理由,,永远不要进行不成熟的优化,直到性能分析器告诉你这一点。

unique_ptr的独占使得它无法被复制(报错),而share_ptr的共享则使得它能被复制,并且多个智能指针能够同时控制:

#include <iostream>
#include<memory>
using namespace std;

int main()
{
    shared_ptr<int> sp1(new int(3));
    cout<<"shared_ptr所有权转交前:"<<sp1.get()<<endl;
    shared_ptr<int> sp2 = move(sp1);
    cout<<"shared_ptr所有权转交后:"<<sp1.get()<<endl;
    shared_ptr<int> sp3 = sp2;    //能够复制,不会报错
    cout<<"shared_ptr所有权复制后 sp2:"<<sp2.get()<<endl;
    cout<<"shared_ptr所有权复制后 sp3:"<<sp3.get()<<endl;
    //shared_ptr<int> sp4(sp3);//能拷贝
}

输出结果:
在这里插入图片描述
shared_ptr的引用计数使用:

#include <iostream>
#include<memory>
using namespace std;

int main()
{
    shared_ptr<int> sp1(new int(3));
    shared_ptr<int> sp2 = sp1;
    shared_ptr<int> sp3 = sp2;

    shared_ptr<int> sp4(new int(3));
    shared_ptr<int> sp5 = sp4;

    //sp1,sp2,sp3 经过复制,共享同一个 shared_ptr,计数器计数为3
    cout<<"sp1 的引用个数计数:"<<sp1.use_count()<<endl;
    cout<<"sp2 的引用个数计数:"<<sp2.use_count()<<endl;
    cout<<"sp3 的引用个数计数:"<<sp3.use_count()<<endl;

    //sp4,sp5 经过复制,共享同一个 shared_ptr,计数器计数为2
    cout<<"sp4 的引用个数计数:"<<sp4.use_count()<<endl;
    cout<<"sp5 的引用个数计数:"<<sp5.use_count()<<endl;
}

输出结果:
在这里插入图片描述

weak_ptr是功能较弱的指针,这个类中没有对 *->的重载,配合shared_ptr用于观测资源的使用。观察者意味着 weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。weak_ptr可以通过lock()返回一个空的shared_ptr()

使用:

share
weak_ptr<T> wp1;	//创建空 weak_ptr,可以指向类型为 T 的对象
weak_ptr<T> w(sp);	//与shared_ptr 指向相同的对象,shared_ptr 引用计数不变。T必须能转换为sp指向的类型
w=p;				//p 可以是 shared_ptr 或 weak_ptr,赋值后 w 与 p 共享对象
w.reset();			//将 w 置空
w.use_count();		//返回与 w 共享对象的 shared_ptr 的数量
w.expired();		//若 w.use_count() 为 0,返回 true,否则返回 false
w.lock();			//如果 expired() 为 true,返回一个空 shared_ptr,否则返回非空 shared_ptr
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值