代码段一:
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/89001718 及 https://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
文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用LoadLibrary
或MFC
提供的AfxLoadLibrary
显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()
获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary
或MFC
提供的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.h
、DllTest.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.dll
和MyDll.lib
拷贝到当前工程所在的目录下面,也可以拷贝到windows
的System
目录下。如果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
:
#if
与if
不同之处在于#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_ptr
是C++11
之前的智能指针,在C++11
之后被弃用,因为auto_ptr
有时在所有权转交后,访问原有对象可能会出现程序异常,但看功能,auto_ptr
与shared_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