指针的形参


函数中传递的是p指针的形参(只能修改arr[1]的值,无法修改*p指向的位置)

upload successful

*p形参的情况:

  • 一开始:
graph LR
    classDef className fill:white,stroke:#333,stroke-width:1px;
    id3[*p 形参]-->id1[7]
    subgraph arr数组
    id[10]-->id1[7]
    id1[7]-->id2[5]
    end
    subgraph *p
    id3[*p 形参]
    end
    class id,id1,id2,id3 className;
  • 最后:
graph LR
    classDef className fill:white,stroke:#333,stroke-width:1px;
    id4[*p]-->id1[3]
    id3[*p 形参]-->id[10]

    subgraph *p 形参
    id3[*p 形参]
    end

    subgraph *p
    id4[*p]
    end    

    subgraph arr数组
    id[10]-->id1[3]
    id1[3]-->id2[5]
    end

    class id,id1,id2,id3,id4 className;
  • 注意:*p--=3实际上是*p=3,然后(p--)
1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
using namespace std;
int func(int *p){
return (*p--=3)-1;
}
int main()
{
int arr[]={10,7,5};
int *p=arr+1;
printf("%d",func(p)+*p);
// 答案为5
}

*&p引用的情况:

  • 一开始:
graph LR
    classDef className fill:white,stroke:#333,stroke-width:1px;
    id3[*&p]-->id1[7]
    subgraph arr数组
    id[10]-->id1[7]
    id1[7]-->id2[5]
    end
    subgraph *&p
    id3[*&p]
    end
    class id,id1,id2,id3 className;
  • 最后:
graph LR
    classDef className fill:white,stroke:#333,stroke-width:1px;
    id4[*p]-->id[10]
    id3[*&p]-->id[10]

    subgraph *&p
    id3[*&p]
    end

    subgraph *p
    id4[*p]
    end    

    subgraph arr数组
    id[10]-->id1[3]
    id1[3]-->id2[5]
    end

    class id,id1,id2,id3,id4 className;
1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
using namespace std;
int func(int *&p){
return (*p--=3)-1;
}
int main()
{
int arr[]={10,7,5};
int *p=arr+1;
printf("%d",func(p)+*p);
// 答案为12
}

#ifndef/#define/#endif

在c语言中,对同一个变量或者函数进行多次声明是不会报错的。所以如果h文件里只是进行了声明工作,即使不使用# ifndef宏定义,多个c文件包含同一个h文件也不会报错。

但是在c++语言中,#ifdef的作用域只是在单个文件中。所以如果h文件里定义了全局变量,即使采用#ifdef宏定义,多个c文件包含同一个h文件还是会出现全局变量重定义的错误。

使用#ifndef可以避免下面这种错误:如果在h文件中定义了全局变量,一个c文件包含同一个h文件多次,如果不加#ifndef宏定义,会出现变量重复定义的错误;如果加了#ifndef,则不会出现这种错误。

其实“被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。比如:存在a.h文件#include “c.h”而此时b.cpp文件导入了#include “a.h” 和#include “c.h”此时就会造成c.h重复引用。

1
2
3
4
5
#ifndef x                 //先测试x是否被宏定义过
#define x
程序段1blabla~ //如果x没有被宏定义过,定义x,并编译程序段 1
#endif
  程序段2blabla~   //如果x已经定义过了则编译程序段2的语句,“忽视”程序段 1

循环引用的后果:

有些头文件重复引用只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些,但是对于大工程而言编译效率低下那将是一件多么痛苦的事情。

有些头文件重复包含,会引起错误,比如在头文件中定义了全局变量(虽然这种方式不被推荐,但确实是C规范允许的)这种会引起重复定义。

volatile

介绍:

volatile是一个类型修饰符(type specifier)volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。

volatile和编译器的优化有关:

在本次线程内,当读取一个变量时,为了提高读取速度,编译器进行优化时有时会先把变量读取到一个寄存器中;以后,再读取变量值时,就直接从寄存器中读取;当变量值在本线程里改变时,会同时把变量的新值copy到该寄存器中,以保持一致。

当变量因别的线程值发生改变,上面寄存器的值不会相应改变,从而造成应用程序读取的值和实际的变量值不一致。

内联函数


函数调用原理:

执行到函数调用指令时,程序将在函数调用后立即存储该指令的内存地址,并将函数参数复制到堆栈(为此保留的内存块),跳到标记函数起点的内存单元,执行函数代码(也许还需将返回值放入寄存器中),然后跳回到地址被保存的指令处。来回跳跃并记录跳跃位置意味着以前使用函数时,需要一定的开销。


内联函数提供了另一种选择。编译器将使用相应的函数代码替换函数调用。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。

1
inline double square(double x){return x*x;}


内联函数与宏定义的区别:

使用宏和内联函数都可以节省在函数调用方面所带来的时间和空间开销。二者都采用了空间换时间的方式,在其调用处进行展开:
(1) 在预编译时期,宏定义在调用处执行字符串的原样替换。在编译时期,内联函数在调用处展开,同时进行参数类型检查。
(2) 内联函数首先是函数,可以像调用普通函数一样调用内联函数。而宏定义往往需要添加很多括号防止歧义,编写更加复杂。
(3) 内联函数可以作为某个类的成员函数,这样可以使用类的保护成员和私有成员。而当一个表达式涉及到类保护成员或私有成员时,宏就不能实现了(无法将this指针放在合适位置)。

可以用内联函数完全替代宏。
在编写内联函数时,函数体应该短小而简洁,不应该包含循环等较复杂结构,否则编译器不会将其当作内联函数看待,而是把它决议成为一个静态函数。

有些编译器甚至会优化内联函数,通常为避免一些不必要拷贝和构造,提高工作效率。

频繁的调用内联函数和宏定义容易造成代码膨胀,消耗更大的内存而造成过多的换页操作。

虚函数

https://blog.csdn.net/zwe7616175/article/details/81334711
https://zhidao.baidu.com/question/123304290.html


说明:

使用虚函数,系统会有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(位于类内其他成员前面),是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联的时间开销很少,提高了多态性的效率。

upload successful

编译器生成的析构函数都是非虚的,除非是一个子类,其父类有个虚析构函数,此时的虚函数特性继承自基类。有虚函数的类,一般情况下要定义一个虚析构函数。

纯虚函数不能new。


构造函数不能声明为虚函数的原因?

  1. 构造一个对象时,必须知道对象实际类型,而虚函数是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功,编译器就无法知道对象的实际类型,是该类本身,还是派生类,还是其他。

  2. 虚函数的执行依赖于虚函数表,而虚函数表是在构造函数中进行初始化的,即初始化虚表指针(vptr),使得正确指向虚函数表。而在构造对象期间,虚函数表(vtable)还没有被初始化,将无法进行。

什么情况下,类的析构函数应该声明为虚函数?为什么?

类A中有了虚函数就会在类的数据成员的最前面添加一个vfptr指针(void** vfptr),这个指针用来指向一个vtable表(一个函数指针数组)(一个类只有一个该表),该表存储着当前类的所有虚函数的地址。这样vfptr就成为了一个类似成员变量的存在。访问虚函数的时候通过vfptr间址找到vtable表,再间址进而找到要调用的函数。这样就在一定程度上摆脱了类型制约。

当B类继承A类的时候,因为A中有虚函数,编译器就自动的给B类添加vfprt指针和vtable表。也可以理解为B类继承来了A类中的那个vptr指针成员。(B对象的vfptr浅拷贝到A对象的vfptr)因此,A对象的vfptr所指向的是B对象的虚函数表,而B的析构函数位于书函数表0的位置,因此,这样就可以通过A类对象的指针d,找到B类对象的析构函数,从而在delete d;时,可以销毁B对象,而不会产生内存泄漏和异常。

  • 简单得来讲:析构函数设为虚函数的原因是为了防止内存泄露。在继承体系中,当基类的指针或引用指向派生类,用基类delete时,如果基类的析构函数没有声明为虚函数,只能析构基类对象,派生类对象将无法析构。因为普通函数受类型的制约,(因为没有vptr指针)使用哪个类的指针调用函数,那么所调用的就是那个累的函数。
    1
    2
    3
    4
    # bug复现
    a* aa= new b(); // b继承a
    delete aa;
    // 打印(执行) a 的析构函数,不执行b的

所以需要将基类的析构函数声明为虚函数,当撤销基类对象的同时也撤销派生类的对象,这个过程是动态关联完成的。


内联函数不能是虚函数

内联函数会在预编译时会进行代码展开,省略函数调用,因此内联函数不能是虚函数。虽然使用inline和virtual共同修饰一个函数时能够通过编译,并在调用时会表现出虚函数的性质,但这是因为编译器在函数声明中遇到virtual关键字时,会选择忽略inline关键字,不进行代码展开。

静态函数不能声明为虚函数

虚函数体现了对象在运行时的多态性,而静态函数属于整个类,不属于某个对象,不能声明为虚函数。

C++编译过程

graph TD
    classDef className fill:white,stroke:#333,stroke-width:1px;
    id(预处理,做优化,生成.i文件)-->id1[编译器,生成.s文件]
    id1[编译器,生成.s文件]-->id2[汇编器,生成.o文件]
    id2[汇编器,生成.o文件]-->id3[链接器,连接库文件和其他目标文件]
    id3[链接器,连接库文件和其他目标文件]-->id4(生成可执行文件)
    class id,id1,id2,id3,id4 className;

C++预处理阶段主要完成的工作:

处理#开始的预编译指令:
(1)宏定义(#define):对宏定义进行字符串替换。
(2)文件包含(#include):文件包含命令把指定头文件插入该命令行位置取代该命令行,从而把指定的文件和当前的源程序文件连成一个源文件。
(3)条件编译(#ifdef):当满足某条件时语句进行编译,而当条件不满足时则编译另一组语句。条件编译对程序的移植和调试是很有用的。

1
2
3
4
5
6
7
#ifndef  Linux
linux平台下运行的函数
#else
#ifndef windows
Windows平台下运行的函数
#endif
#endif

汇编阶段:

直接生成了汇编代码

链接器阶段

(1)函数重载:同名函数,不同函数输入变量/输出变量的类型/不同功能。
(2)inline函数:如果函数体不太大,对此函数的所有调用都以函数本体去替代,注意inline只是对编译器的一个建议申请,不是强制命令
(3)模板处理:函数定义(包括具现化后的函数模板,类模板的成员函数),变量定义(包括函数模板的静态数据变量,类模板的静态数据变量,类模板的全局对象等)
(4)虚函数:每一个多态class都有一份虚函数表,定义或继承了虚函数的对象会有一个隐含成员:指向虚表的指针vptr,在构造或析构对象的时候,编译器生成的代码会修改这个指针。按道理说,一个多态class的虚表应该恰好被一个目标文件定义,这样链接就不会有错,但c++编译器有时无法判断是否应该在当前编译单元生成虚表定义,为保险起见,只能每个编译单元都生成虚表,然后交给链接器来消除重复数据。

  • C++使用的也是C语言的单遍编译的方式,从头到尾扫描一遍源码,一边解析源码,一边即刻生成目标代码

特征工程-特征归一化

为什么要归一化?

在实际应用中,通过梯度下降一类的方法需要进行归一化,比如逻辑回归,线性回归,SVM,神经网络,Adaboost,SVM,LR,Knn,KMeans等。但是决策树,朴素贝叶斯,隐马尔可夫等模型不需要归一化。因为前者是基于数值的决策,对单位敏感度高,如果不进行归一化操作可能会导致x/y中有一个变量的影响力$\uparrow$,可能需要更多轮次的迭代才能得到最终解。后者基于交叉熵的信息增益,基于概率分布模型,而概率本身是归一化的(%),所以不需要归一化。

![upload successful](\images\pasted-1.png)

逻辑回归一定要归一化么?

如果你用了L1L2正则的话需要,因为不用正则时,我们的损失函数只是仅仅在度量预测与真实的差距,加上正则后,我们的损失函数除了要度量上面的差距外,还要度量参数值是否足够小。而参数值的大小程度是与特征的数值范围相关的。

归一化的方法

  1. 线性函数归一化(适用于简单数值):$X=\frac{X-X_{min}}{X_{max}-X_{min}}$
  2. 0均值归一化:$z=\frac{x-u}{\sigma}$
| 本站总访问量次 ,本文总阅读量