2018-11-15


C/C++ -------

C/C++程序编译过程

预处理(Preprocessing):主要用于处理#开头的代码行,比如对宏做展开,对include的文件做展开,条件编译选项判断,清理注释等。文件以.i和.ii结尾。 编译(Compilation):使用预处理的输出结果作为输入,生成文本格式的平台相关的汇编代码(assembly code)。文件以.s结尾。 汇编(Assemble):将上一步的汇编代码转换成二进制的机器码,称为object code。产生的文件叫做目标文件,是二进制格式,文件以.o或.obj结尾。 链接(Linking):链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)

步骤命令等价命令输出文件
预处理cppgcc -E.i, .ii
编译cc1, cc1plusgcc -S.s
汇编asgcc -c.o, .obj
链接ldgcc可执行文件

函数指针的定义

函数返回值类型 (* 指针变量名) (函数参数列表);

获取实例对象的函数指针

注意调用类中非静态成员函数的时候, 使用的是类名::函数名; 而不是实例名::函数名;

 
class A 
{
public:
    static void staticmember(){cout<<"static"<<endl;}   //static member
    void nonstatic(){cout<<"nonstatic"<<endl;}          //nonstatic member
    virtual void virtualmember(){cout<<"virtual"<<endl;};//virtual member
};
int main()
{
    A a;
    //static成员函数,取得的是该函数在内存中的实际地址, 而且因为static成员是全局的, 所以不能用A::限定符
    void (*ptrstatic)() = &A::staticmember;      
    //nonstatic成员函数 取得的是该函数在内存中的实际地址     
    void (A::*ptrnonstatic)() = &A::nonstatic;
    //虚函数取得的是虚函数表中的偏移值, 这样可以保证能过指针调用时同样的多态效果
    void (A::*ptrvirtual)() = &A::virtualmember;
    //函数指针的使用方式
    ptrstatic();
    (a.*ptrnonstatic)();
    (a.*ptrvirtual)();
}

C++ 获取类中成员函数的函数指针

Lambda 函数定义

参数

[](int x, int y) { return x + y; } // 隐式返回类型
[](int& x) { ++x; } // 没有 return 语句 -> lambda 函数的返回类型是'void'
[]{ ++global_x; }   // 没有参数,仅访问某个全局变量
{ ++global_x; }     // 与上一个相同, 省略了[]
 
[]  //未定义变量.试图在Lambda内使用任何外部变量都是错误的.
[&] //用到的任何外部变量都隐式按引用捕获
[=] //用到的任何外部变量都隐式按值捕获
[x, &y] // x 按值捕获, y 按引用捕获.
[&, x]  // x 显式地按值捕获. 其它变量按引用捕获
[=, &z] // z 按引用捕获. 其它变量按值捕获

C++11 的STD::FUNCTION

利用继承与多态, 我们可以让编译器帮我们搞定函数对象的析构; 就这种实现而言, 这是简洁而有效的方法; 然而这种实现需要动态内存, 在一些情况下不划算, 甚至完全没有必要;

C++11引入了lambda表达式, 其本质也是函数对象; 这个对象有多大呢?取决于捕获列表; 你 写lambda会捕获多少东西?很多情况下就只是一对方括号而已吧; 在这种情况下, lambda表达式的对象大小只有1字节(因为不能是0字节),你却为了这没有内容的1字节要调用动态内存的函数?C++注重运行时效率, 这种浪费是不能接受的;

如何避免这种浪费呢?你也许会说我检查传入的对象是不是1字节的空类型; 且不论这个trait怎么实现, 函数指针, 捕获一个int的lambda等类型都声称自己是trivial的小对象, 也不应该分配到heap中去;

之前说过, std::function 的大小是固定的, 但是这个大小是可以自己定的;我们可以在 std::function 的类定义中加入一个空白的, 大小适中的field, 用在存放这些小对象, 从而避免这些情况下的动态内存操作; 同时, 既然有了这片空间, 也就不需要看传入的对象是不是1字节的空类型了;

而对于更大的类型,虽然这个field不足以存放函数对象, 但足以存放一个指针, 这种分时复用的结构可以用union来实现;这种小对象直接存储, 大对象在heap上开辟空间并存储指针的方法, 称为small object optimization;

STD::FUNCTION接口与实现

在C中 用c++类成员函数 作为回调函数?

普通的C++成员函数都隐含了一个传递函数作为参数, 亦即 this 指针, C++通过传递一个指向自身的指针给其成员函数从而实现程序函数可以访问C++的数据成员; 这也可以理解为什么C++类的多个实例可以共享成员函数但是确有不同的数据成员;

由于this指针的作用, 使得将一个CALLBACK型的成员函数作为回调函数安装时就会因为隐含的this指针使得函数参数个数不匹配, 从而导致回调函数安装失败;

c++11 中将类成员函数实现回调

C++11中提供了std::bind, 可以说是一种飞跃的提升, bind本身是一种延迟计算的思想, 它本身可以绑定普通函数, 全局函数, 静态函数, 类静态函数甚至是类成员函数;

类似javascript的 bind,可以实现参数顺序调整等操作

std::bind

可将std::bind函数看作一个通用的函数适配器, 它接受一个可调用对象, 生成一个新的可调用对象来”适应”原对象的参数列表;

std::bind 将可调用对象与其参数一起进行绑定, 绑定后的结果可以使用std::function保存; std::bind主要有以下两个作用:

  • 将可调用对象和其参数绑定成一个防函数;
  • 只绑定部分参数, 减少可调用对象传入的参数;

std::bind绑定普通函数

double my_divide (double x, double y) {return x/y;}
auto fn_half = std::bind (my_divide,_1,2);  
std::cout << fn_half(10) << '\n';                        // 5
  • ind的第一个参数是函数名, 普通函数做实参时, 会隐式转换成函数指针; 因此std::bind (my_divide,_1,2)等价于std::bind (&my_divide,_1,2);

  • _1 表示参数占位符, 位于 <functional>中, std::placeholders::_1

std::bind绑定一个成员函数

struct Foo {
    void print_sum(int n1, int n2)
    {
        std::cout << n1+n2 << '\n';
    }
    int data = 10;
};
int main() 
{
    Foo foo;
    auto f = std::bind(&Foo::print_sum, &foo, 95, std::placeholders::_1); //相当于 生成 void(int), 原始的第一个参数写死95
    f(5); // 100
}
  • bind绑定类成员函数时, 第一个参数表示对象的成员函数的指针, 第二个参数表示对象的地址;

  • 必须显示的指定&Foo::print_sum, 因为编译器不会将对象的成员函数隐式转换成函数指针, 所以必须在Foo::print_sum前添加&;

  • 使用对象成员函数的指针时, 必须要知道该指针属于哪个对象, 因此第二个参数为对象的地址 &foo;

C++11 中的std::function和std::bind

海康回调的问题

 std::bind(SDKSource::realDataCallBack, this
        std::placeholders::_1,std::placeholders::_2,std::placeholders::_3,std::placeholders::_4);

no matching function 错误

__stdcall 修饰的函数, (C标准)纯函数指针, 因为std::bind的实例是包含额外信息的, 而函数指针只是一个指向一段代码的指针, 编译器无法转换;

正解是…

不管怎么包装, 都需要把额外的this指针传给那个要注册的callback, 但是原生的函数指针是无法携带这个额外的信息的, um.. 此题无解!

但是… SDK还有另外有个NET_DVR_SetStandardDataCallBackEx 回调! 其定义是: NET_DVR_API BOOL __stdcall NET_DVR_SetStandardDataCallBackEx(LONG lRealHandle,void(CALLBACK *fStdDataCallBack) (LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer,DWORD dwBufSize,void *pUser),void *pUser);

其中第三个形参是void *, so !!!

程序信号

C++ 信号处理

信号是由操作系统传给进程的中断,会提早终止一个程序。在 UNIX、LINUX、Mac OS X 或 Windows 系统上,可以通过按 Ctrl+C 产生中断。

信号描述
SIGABRT程序的异常终止,如调用abort 。
SIGFPE错误的算术运算,比如除以零或导致溢出的操作。
SIGILL检测非法指令。
SIGINT程序终止(interrupt)信号。
SIGSEGV非法访问内存。
SIGTERM发送到程序的终止请求。

C++ 信号处理库提供了 signal 函数,用来捕获突发事件

signal(registered signal, signal handler)

这个函数接收两个参数:第一个参数是一个整数,代表了信号的编号;第二个参数是一个指向信号处理函数的指针。

浅谈 C++ 中的接口、纯虚函数与虚析构函数

接口的本质与实现

在C++中,接口是通过纯虚类(Pure Abstract Class)实现的,这是一种只包含纯虚函数而没有数据成员的特殊抽象类。

  1. ​纯虚函数声明​​:virtual QString getCommandCode() = 0;
  2. ​抽象类性质​​:只要类中包含至少一个纯虚函数,该类就成为抽象类,不能被直接实例化
  3. ​虚析构函数必要性​​:必须提供虚析构函数(即使函数体为空)
class CommandAccept {
public:
    CommandAccept() {}  // 基类需要构造函数体
    virtual ~CommandAccept() {}  // 虚析构函数,必须要有函数体!
    
    virtual QString getCommandCode() = 0;  // 纯虚函数
    virtual Response process(Session* session, Request request) = 0;
};

虚析构函数的关键作用

CommandAccept* crecord = new CommandAcceptRecord();
delete crecord;  // 如果基类析构函数非虚,将不会调用派生类析构函数

内存泄漏风险​​:当基类指针指向派生类对象时,如果基类析构函数不是虚函数:

  • 只会调用基类的析构函数
  • 派生类的析构函数不会被调用
  • 派生类分配的资源将泄漏
virtual ~CommandAccept() = default; // C++11风格,明确使用默认实现

接口类的设计规范

  1. ​纯虚函数规范​​:
    • 使用= 0语法声明
    • 派生类必须实现所有纯虚函数
    • 可以包含多个纯虚函数
  2. ​构造与析构​​:
    • 提供默认构造函数(即使为空)
    • ​必须​​提供虚析构函数
    • 通常不需要拷贝构造函数和赋值运算符(接口类一般不应被拷贝)
  3. ​实现方式​​:
    • 通常只有头文件(.h),没有实现文件(.cpp)
    • 纯虚函数不应在基类中提供实现(少数特殊情况除外)
概念关键点语法示例注意事项
​接口类​纯虚函数集合class I { virtual void f() = 0; };不能被实例化
​纯虚函数​强制派生类实现virtual void f() = 0;可提供默认实现(罕见)
​虚析构函数​确保正确析构virtual ~Base() = default;必须提供,即使为空
​override​明确重写意图void f() override;C++11引入,提高安全性
​final​禁止进一步重写virtual void f() final;用于类或方法

运算符重载

C++ 重载运算符和重载函数

public:
Distance operator- () // -
int& operator[](int i)//[]
friend istream &operator>>( istream  &input, Distance &D ) // >>
Time operator++( int )// ++
 

new/delete 和 malloc/free

new和malloc的区别

new 是一个运算符, malloc()是一个库函数; new 会调用构造函数, 而malloc()不会; new 返回指定类型的指针, 而malloc()返回void*; new 会自动计算需要分配的空间, 而malloc()需要手工计算字节数; new 可以被重载, 而 malloc()不能;

delete和free的区别

delete 是一个运算符, free()是一个库函数; delete 会调用析构函数, 而free()不会; delete 可以被重载, 而free()不能;

struct 在C++中

成员的默认访问权限; class的成员默认是private权限, struct默认是public权限;

除了这两点, class和struct基本就是一个东西; 语法上没有任何其它区别;

链接库

静态库

是包含函数代码本身, 在编译时直接将代码加入程序当中, 称为静态链接库static link library;

优点: 整个程序可以只要一个可执行文件就行了

缺点: 可执行文件会很大很大~

linux 通常以libxxx.a后缀 win 通常以.lib后缀

动态库

是包含了函数所在的DLL文件和文件中函数位置的信息(入口), 代码由运行时加载在进程空间中的DLL提供, 称为动态链接库dynamic link library;

优点: 一方面是缩小了执行文件本身的体积, 另一方面是加快了编译速度, 节省了系统资源;

缺点: 一是哪怕是很简单的程序, 只用到了链接库中的一两条命令, 也需要附带一个相对庞大的链接库; 二是如果其他计算机上没有安装对应的运行库, 则用动态编译的可执行文件就不能运行;

linux 通常以libxxx.so后缀 win 通常以.dll后缀

GOT表

简而言之, 动态库其实是将链接过程延迟到运行时;

多个动态lib时, 由于lib是动态加载, 编译器无法确定调用的函数地址; 例如一个调用动态库 printf函数的代码, 会被编译成 callq 0x4003c4 <printf@plt> 这里的0x4003c4其实是GOT表的地址

  • 当指令需要访问变量或者函数的地址时,从对应的GOT表项中读出地址,再访问即可。对应的指令可能是callq *(addr in GOT)或者movq offset(%rip) %rax(%rax就是全局变量的地址,可以用(%rax)解引用)。
  • 当动态链接器装载共享对象时查找每一个需要重定位符号的变量地址,填充GOT。
  • 为每一个需要重定位的符号建立一个GOT表项。

但是这样有一个问题,一个动态库可能有成百上千个符号,但是我们引入该动态库可能只会使用其中某几个符号,像上面那种方式就会造成不使用的符号也会进行重定位,造成不必要的效率损失。 所以ELF采用了延迟绑定的技术,当函数第一次被用到时才进行绑定。实现方式就是使用plt(类似增强的方式, 避免生成所有函数的got)

windows下的lib/dll

如果有dll和lib 文件(动态库), 那么lib一般是一些索引信息, 记录了dll中函数的入口和位置, dll中是函数的具体实现内容;

使用dll(动态)需注意三个文件: (1).h头文件, 包含dll中说明输出的类或符号原型或数据结构的.h文件; 应用程序调用dll时, 需要将该文件包含入应用程序的源文件中; (2).LIB文件, 是dll在编译, 链接成功之后生成的文件, 作用是当其他应用程序调用dll时, 需要将该文件引入应用程序, 否则产生错误;如果不想用lib文件或者没有lib文件, 可以用WIN32 API函数LoadLibrary, GetProcAddress装载; (3).dll文件, 真正的可执行文件, 开发成功后的应用程序在发布时, 只需要有.exe文件和.dll文件, 并不需要.lib和.h头文件;

如果只有lib文件(静态库), 那么这个lib文件是静态编译(静态库)出来的,索引和实现都在其中; 使用lib(静态)需注意两个文件: (1).h头文件, 包含lib中说明输出的类或符号原型或数据结构; 应用程序调用lib时, 需要将该文件包含入应用程序的源文件中; (2).LIB文件, 略;

Windows 系统如何查找 动态库?

使用LoadLibrary显式链接, 那么在函数的参数中可以指定DLL文件的完整路径;如果不指定路径, 或者进行隐式链接, Windows将遵循下面的搜索顺序来定位DLL: (1)包含EXE文件的目录 (2)工程目录 (3)Windows系统目录 (4)Windows目录 (5)列在Path环境变量中的一系列目录

windows下LIB和DLL的区别与使用

Linux 系统如何查找 动态库?

  1. 从系统目录: /usr/local/lib/
  2. 从环境变量: LD_LIBRARY_PATH

export LD_LIBRARY_PATH

列出程序依赖的so库, ldd 是 list dynamic dependencies的缩写 ldd ffmpeg


C 获取当前目录

char cmd[1024];
cmd[0] = '\0';
strcat(cmd, "cmd.exe /c \"");
 
///
char cpath[600] ;
if(getcwd(cpath,  600) == NULL){
    printf("get cpath fail!");
    return -1;
}
printf("cpath is: [%s]\n", cpath);
strcat(cmd, cpath);
////
strcat(cmd, "\\start.bat\"");
printf("cmd is: [%s]\n", cmd);
system(cmd);


MinGW 编译器 -------

MinGW 官网

sourceforge MinGW32 版本归档 sourceforge MinGW64 版本归档 下载器里可以选择版本,32/64/, GDB 等…

gcc 官网 GCC编译器30分钟入门教程


Windowns 服务程序开发(c/c++) -------

全局定义

SERVICE_STATUS ServiceStatus; 
SERVICE_STATUS_HANDLE hStatus; 
 #define SERVER_NAME "jk-jy-print-server"
 
void ServiceMain(int argc, char** argv); 
void ControlHandler(DWORD request); 

声明几个全局变量, 以便在程序的多个函数之间共享它们值;

程序入口点/ SERVICE_TABLE_ENTRY

//程序入口点
int main(int argc, char* argv[])
{
    SERVICE_TABLE_ENTRY ServiceTable[2];
    ServiceTable[0].lpServiceName = SERVER_NAME;
    ServiceTable[0].lpServiceProc = (LPSERVICE_MAIN_FUNCTION)ServiceMain;
 
    ServiceTable[1].lpServiceName = NULL;
    ServiceTable[1].lpServiceProc = NULL;
    // 启动服务的控制分派机线程
    StartServiceCtrlDispatcher(ServiceTable);
}

一个程序可能包含若干个服务; 每一个服务都必须列于专门的分派表中(为此该程序定义了一个 ServiceTable 结构数组); 这个表中的每一项都要在 SERVICE_TABLE_ENTRY 结构之中;

它有两个域: lpServiceName: 指向表示服务名称字符串的指针;当定义了多个服务时, 那么这个域必须指定; lpServiceProc: 指向服务主函数的指针(服务入口点);

LPSERVICE_MAIN_FUNCTION

 
int InitService()
{
    return TRUE;
}
 
//服务入口点, 由StartServiceCtrlDispatcher 分派
void ServiceMain(int argc, char** argv)
{
   int error;
 
   ServiceStatus.dwServiceType = SERVICE_WIN32;
   ServiceStatus.dwCurrentState = SERVICE_START_PENDING;
   ServiceStatus.dwControlsAccepted =SERVICE_ACCEPT_STOP |SERVICE_ACCEPT_SHUTDOWN;
   ServiceStatus.dwWin32ExitCode = 0;
   ServiceStatus.dwServiceSpecificExitCode = 0;
   ServiceStatus.dwCheckPoint = 0;
   ServiceStatus.dwWaitHint = 0;
 
   hStatus = RegisterServiceCtrlHandler( SERVER_NAME, (LPHANDLER_FUNCTION)ControlHandler);
   if (hStatus == (SERVICE_STATUS_HANDLE)0)
   {
      // Registering Control Handler failed
      return;
   }
   // Initialize Service 初始化服务
   error = InitService();
   if (error)
   {
      // Initialization failed
      ServiceStatus.dwCurrentState = SERVICE_STOPPED;
      ServiceStatus.dwWin32ExitCode = -1;
      //报告失败
      SetServiceStatus(hStatus, &ServiceStatus);
      return;
   }
   // We report the running status to SCM.
   ServiceStatus.dwCurrentState = SERVICE_RUNNING;
   SetServiceStatus (hStatus, &ServiceStatus);
 
   MEMORYSTATUS memory;
   // The worker loop of a service 服务工作线程循环
   while (ServiceStatus.dwCurrentState == SERVICE_RUNNING)
   {
      char buffer[16];
      GlobalMemoryStatus(&memory);
      sprintf(buffer, "%d", memory.dwAvailPhys);
      //这里示例 写文件
      int result = WriteToLog(buffer);
      if (result)
      {
         ServiceStatus.dwCurrentState =  SERVICE_STOPPED;
         ServiceStatus.dwWin32ExitCode = -1;
         SetServiceStatus(hStatus,  &ServiceStatus);
         return;
      }
      Sleep(5000);
   }
   return;
}
 

该函数是服务的入口点; 它运行在一个单独的线程当中, 这个线程是由控制分派器创建的;

应该尽可能早早为服务注册控制处理器; 这要通过调用RegisterServiceCtrlHadler 函数来实现; 你要将两个参数传递给此函数: 服务名和指向 ControlHandlerfunction 的指针;

它指示控制分派器调用 ControlHandler 函数处理 SCM 控制请求; 注册完控制处理器之后, 获得状态句柄(hStatus); 通过调用 SetServiceStatus 函数, 用 hStatus 向 SCM 报告服务的状态;

dwServiceType: 指示服务类型, 创建 Win32 服务; 赋值 SERVICE_WIN32 ; dwCurrentState: 指定服务的当前状态; 因为服务的初始化在这里没有完成, 所以这里的状态为 SERVICE_START_PENDING ; dwControlsAccepted: 这个域通知 SCM 服务接受哪个域; 本文例子是允许 STOP 和 SHUTDOWN 请求; 处理控制请求将在第三步讨论; dwWin32ExitCode 和 dwServiceSpecificExitCode: 这两个域在你终止服务并报告退出细节时很有用; 初始化服务时并不退出, 因此, 它们的值为 0 ; dwCheckPoint 和 dwWaitHint: 这两个域表示初始化某个服务进程时要30 秒以上; 本文例子服务的初始化过程很短, 所以这两个域的值都为 0 ;

调用 SetServiceStatus 函数向 SCM 报告服务的状态时; 要提供 hStatus 句柄和 ServiceStatus 结构; 注意 ServiceStatus 一个全局变量, 所以你可以跨多个函数使用它; ServiceMain 函数中, 你给结构的几个域赋值, 它们在服务运行的整个过程中都保持不变, 比如: dwServiceType ;

LPHANDLER_FUNCTION

void ControlHandler(DWORD request) 
{ 
    switch(request) 
    { 
    case SERVICE_CONTROL_STOP: 
        WriteToLog("Monitoring stopped.");
        ServiceStatus.dwWin32ExitCode = 0; 
        ServiceStatus.dwCurrentState = SERVICE_STOPPED; 
        SetServiceStatus (hStatus, &ServiceStatus);
        return; 
    case SERVICE_CONTROL_SHUTDOWN: 
        WriteToLog("Monitoring stopped.");
        ServiceStatus.dwWin32ExitCode = 0; 
        ServiceStatus.dwCurrentState = SERVICE_STOPPED; 
        SetServiceStatus (hStatus, &ServiceStatus);
        return; 
    default:
        break;
    } 
    // Report current status
    SetServiceStatus (hStatus, &ServiceStatus);
    return; 
}

ServiceMain的 RegisterServiceCtrlHandler 函数注册了控制处理器函数; 控制处理器与处理各种 Windows 消息的窗口回调函数非常类似; 它检查 SCM 发送了什么请求并采取相应行动;
每次你调用 SetServiceStatus 函数的时候, 必须指定服务接收STOPSHUTDOWN 请求;

STOP 请求是 SCM 终止服务的时候发送的; 例如, 如果用户在” 服务” 控制面板中手动终止服务; SHUTDOWN 请求是关闭机器时, 由 SCM 发送给所有运行中服务的请求; 两种情况的处理方式相同: 写日志文件, 监视停止;

向 SCM 报告 SERVICE_STOPPED 状态:

由于 ServiceStatus 结构对于整个程序而言为全局量, ServiceStatus 中的工作循环在当前状态改变或服务终止后停止; 其它的控制请求如: PAUSE 和 CONTINUE 在本文的例子没有处理;
控制处理器函数必须报告服务状态, 即便 SCM 每次发送控制请求的时候状态保持相同; 因此, 不管响应什么请求, 都要调用 SetServiceStatus ;

安装服务

安装配置服务

  1. 开始 运行cmd回车
  2. 输入 sc create test binPath= "f:\test\call.exe"(注意: =” 后面是必须空格, 否则会出现错误, test 为创建的服务名)
  3. 启动服务: net start 服务名称
  4. 暂停服务: net stop 服务名称
  5. 删除服务: sc delete 服务名称

有个坑 不能用在 powershell 执行!

C语言开发Windows服务

官文文档-开发 Windows 服务应用(依赖.NET)