C/C++学习笔记:基础知识4

来源:互联网 发布:淘宝宝贝详情怎么做的 编辑:程序博客网 时间:2024/05/16 15:34
1 引用

引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。 申明一个引用的时候, 切记要对其进行初始化。 引用声明完毕后, 相当于目标变量名有两个名称, 即该目标原名称和引用名,不再把该引用名作为其他变量名的别名。 声明一个引用, 不是新定义了一个变量, 它只表示该引 用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不建立数组的引用。

引用是除指针外另一个可以产生多态效果的手段。这意味着, 一个基类的引用可以指向它的派生类实例.

Class A; Class B : Class A{...}; B b; A& ref = b;  // 用派生类对象初始化基类对象的引用


2 “引用”作为函数参数
(1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用, 所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。
(2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作; 而使用一般变量传递函数的参数,当发生函数调用时, 需要给形参分配存 储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。 因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空 间都好。
(3)使用指针作为函数的参数虽然也达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用"*指针变量名"的形式进行运 算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

指针通过某个指针变量指向一个对象后, 对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名, 对引用的操作就是对目标变量的操作。


3 使用“常引用”
如果既要利用引用提高程序的效率, 又要保护传递给函数的数据不在函数中被改变,就应使用常引用。
常引用声明方式:const 类型标识符&引用名=目标变量名;
例 1 
int a ; 
const int &ra=a; 
ra=1; //错误
a=1; //正确


例 2 
string foo( ); 
void bar(string & s); 
那么下面的表达式将是非法的:
bar(foo( )); 
bar("hello world"); 
原因在于 foo( )和"hello world"串都会产生一个临时对象,而在 C++中,这些临时对象都是 const 类型的。 因此上面的表达式就是试图将一个 const 类型的对象转换为非 const 类型,这是非法的。引用型参数应该在能被定义为 const 的情况下,尽量定义为 const 。


4 “引用”作为函数返回值类型
格式:类型标识符&函数名(形参列表及类型说明){ //函数体} 
好处:在内存中不产生被返回值的副本; (注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。 因为随着该局部变量生存期的结束, 相应的引用也会失效,产生 runtime error! 
注意事项:
(1)不能返回局部变量的引用。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了"无所指"的引用,程序会进入未知状态。
(2) 不能返回函数内部 new 分配的内存的引用。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部 new 分配内存的引用) , 又面临其它尴尬局面。 例如, 被函数返回的引用只是作为一 个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由 new 分配)就无法释放,造成 memory leak。
(3)可以返回类成员的引用,但最好是 const。 主要原因是当对象的属性是与某种业务规则 (business  rule)相关联的时候, 其赋值常常与某些其它属性或者对象的状态有关, 因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常 量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。
(4)流操作符重载返回值申明为“引用”的作用:
流操作符<<和>>,这两个操作符常常希望被连续使用, 例如: cout  <<  "hello"  << endl;  

因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。 可选的其它方案包括: 返回一个流对象和返回一个流对象指针。 但是对于返回 一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一 个流指针则不能连续使用<<操作符。 因此, 返回一个流对象引用是惟一选择。 这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这 就是 C++语言中引入引用这个概念的原因吧。赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x  =  j  =  10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。 因此引用成了这个操作符的惟一返回值选择。

#include <iostream>using namespace std;int &put(int n); int vals[10]; int error=-1; void main() { put(0)=10; //以 put(0)函数值作为左值,等价于 vals[0]=10; put(9)=20; //以 put(9)函数值作为左值,等价于 vals[9]=20; } int &put(int n) {      if (n>=0 && n<=9 )              return vals[n];       else      {             cout<<"subscript error";              return error;        } } 

(5) 在另外的一些操作符中, 却千万不能返回引用: +-*/ 四则运算符。 

它们不能返回引用,主要原因是这四个操作符没有 side  effect,因此,它们必须构造一个对象作为返回值,可选的方案包括: 返回一个对象、 返回一个局部变量的引用, 返回一个 new 分配的对象的引用、返回一 个静态对象引用。根据前面提到的引用作为返回值的三个规则,第 2、3 两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为 true 而导致错误。所以可选的只剩下返回一个对象了。

       流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。

总结:

(1)在引用的使用中,单纯给某个变量取个别名是毫无意义的,引用的目的主要用于在函数参数传递中,解决大块数据或对象的传递效率和空间不如意的问题。
(2)用引用传递函数的参数,能保证参数传递中不产生副本,提高传递的效率,且通过const的使用,保证了引用传递的安全性。
(3)引用与指针的区别是,指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。
(4)使用引用的时机。流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。


5 结构与联合

(1)结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员 (所有成员共用一块地址空间) , 而结构的所有成
员都存在(不同成员的存放地址不同)。

(2)对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。


附:struct  和  class  的区别

    struct  的成员默认是公有的,而类的成员默认是私有的。struct  和  class  在其他方面是功能相当的。

    struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的 

    强调struct是一种数据结构的实现体,虽然它是可以像class一样的用,将struct里的变量叫数据,class内的变量叫成员,虽然它们并无区别。当你觉得你要做的更像是一种数据结构的话,那么用struct,如果你要做的更像是一种对象的话,那么用class。 对于访问控制,应该在程序里明确的指出,而不是依靠默认,这是一个良好的习惯,也让你的代码更具可读性。 

 “class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。这一点在Stanley B.Lippman写的Inside the C++ Object Model有过说明。 
C++中的struct是对C中的struct的扩充,既然是扩充,那么它就要兼容过去C中struct应有的所有特性。例如你可以这样写: 

struct A //定义一个struct{   char c1;   int n2;   double db3;};A a={'p', 7, 3.1415926}; //定义时直接赋值 
也就是说struct可以在定义的时候用{}赋初值。
将上面的struct改成class,报错!!试着向上面的struct中加入一个构造函数(或虚函数),对struct也不能用{}赋初值。

的确,以{}的方式来赋初值,只是用一个初始化列表来对数据进行按顺序的初始化,如上面如果写成A a={'p',7};则c1,n2被初始化,而db3没有。这样简单的copy操作,只能发生在简单的数据结构上,而不应该放在对象上。加入一个构造函数或是一个虚函数会使struct更体现出一种对象的特性,而使此{}操作不再有效。 

事实上,是因为加入这样的函数,使得类的内部结构发生了变化。而加入一个普通的成员函数呢?你会发现{}依旧可用。其实你可以将普通的函数理解成对数据结构的一种算法,这并不打破它数据结构的特性。 

发现即使是struct想用{}来赋初值,它也必须满足很多的约束条件,这些条件实际上就是让struct更体现出一种数据机构而不是类的特性。 

那为什么我们在上面仅仅将struct改成class,{}就不能用了呢?其实问题恰巧是访问控制!将struct改成class的时候,访问控制由public变为private了,那当然就不能用{}来赋初值了。加上一个public,你会发现,class也是能用{}的,和struct毫无区别!!! 

总结,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体


在C++的struct和class区别:对于成员访问权限以及继承方式,class默认是private,而struct默认是public。class还可以用于表示模板类型,struct则不行。

C中的stuct与C++的class区别:struct只是作为一种复杂数据结构类型定义,不能面向对象编程。


6 .h 头文件中的 ifndef / define / endif 的作用

防止该头文件被重复引用


7 #include <file.h>与 #include "file.h"的区别

前者是从 Standard  Library 的路径寻找和引用 file.h,而后者是从当前工作路径搜寻并引用 file.h


在 C++ 程序中调用被 C 编译器编译后的函数,要加 extern “C”

        首先,作为 extern 是 C/C++语言中表明函数和全局变量作用范围(可见性)的关键字, 该关键字告诉编译器, 其声明的函数和变量可以在本模块或其它模块中使用。
通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字 extern 声明。 例如,如果模块 B 欲引用该模块 A 中定义的全局变量和函数时 只需包含模块 A 的头文件即可。 这样, 模块 B 中调用模块 A 中的函数时,在编译阶段,模块 B 虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块 A
编译 生成的目标代码中找到此函数extern "C"是连接申明(linkage declaration),被 extern "C"修饰的变量和函数是按照 C 语言方式编译和连接的,来看看 C++中对类似 C 的函数是怎样编译的:

 作为一种面向对象的语言,C++支持函数重载,而过程式语言 C 则不支持。函数被 C++编译后在符号库中的名字与 C 语言的不同。 例如, 假设某个函数的原型为:
    void foo( int x, int y ); 
  该函数被 C 编译器编译后在符号库中的名字为_foo,而 C++编译器则会产生像_foo_int_int 之类的名字(不同的编译器可能生成的名字不同,但是都采用了
相同的机制,生成的新名字称为“mangled name”)。_foo_int_int 这样的名字包含了函数名、 函数参数数量及类型信息, C++就是靠这种机制来实现函数重载的。例如,在 C++中,函数 void  foo(  int  x,  int  y)与 void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。

同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名, 我们以"."来区分。 而本质上,  编译器在进行编译时, 与函数的处理相似, 也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。未加 extern "C"声明时的连接方式

假设在 C++中,模块 A 的头文件如下:

// 模块 A 头文件  moduleA.h #ifndef MODULE_A_H #define MODULE_A_H int foo( int x, int y ); #endif在模块 B 中引用该函数:// 模块 B 实现文件  moduleB.cpp #include "moduleA.h"    foo(2,3); 
实际上,在连接阶段,连接器会从模块 A 生成的目标文件 moduleA.obj 中寻找
_foo_int_int 这样的符号!

加 extern "C"声明后的编译和连接方式

加 extern "C"声明后,模块 A 的头文件变为:// 模块 A 头文件  moduleA.h #ifndef MODULE_A_H #define MODULE_A_H extern "C" int foo( int x, int y ); #endif
在模块 B 的实现文件中仍然调用 foo( 2,3 ),其结果是:
(1)模块 A 编译生成 foo 的目标代码时,没有对其名字进行特殊处理,采用了C 语言的方式;
(2)连接器在为模块 B 的目标代码寻找 foo(2,3)调用时,寻找的是未经修改的符号名_foo。

如果在模块 A 中函数声明了 foo 为 extern  "C"类型, 而模块 B 中包含的是 extern int foo( int x, int y ) ,则模块 B 找不到模块 A 中的函数;反之亦然。
所以,可以用一句话概括 extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的, 来源于真实世界的需求驱动。 我们在思考问题时,不只停留在这个语言是怎么 做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现 C++与 C 及其它语言的混合编程。

明白了 C++中 extern "C"的设立动机,我们下面来具体分析 extern "C"通常的使用技巧:

extern "C"的惯用法
(1)在 C++中引用 C 语言中的函数和变量,在包含 C 语言头文件(假设为cExample.h)时,需进行下列处理:

extern "C" {     #include "cExample.h" } 
而在 C 语言的头文件中,对其外部函数只能指定为 extern 类型,C 语言中不支持 extern "C"声明,在.c 文件中包含了 extern "C"时会出现编译语法错误。C++引用 C 函数例子工程中包含的三个文件的源代码如下:
/* c 语言头文件:cExample.h */ #ifndef C_EXAMPLE_H #define C_EXAMPLE_H extern int add(int x,int y); #endif /* c 语言实现文件:cExample.c */ #i nclude "cExample.h" int add( int x, int y ) {     return x + y; } // c++实现文件,调用 add:cppFile.cpp extern "C" { #include "cExample.h" } int main(int argc, char* argv[]) {    add(2,3);    return 0; } 

如果 C++调用一个 C 语言编写的.DLL 时,当包括.DLL 的头文件或声明接口函数时,应加 extern "C" {}。

(2)在 C 中引用 C++语言中的函数和变量时,C++的头文件需添加 extern "C",但是在 C 语言中不能直接引用声明了 extern "C"的该头文件,应该仅将 C 文件
中将 C++中定义的 extern "C"函数声明为 extern 类型。C 引用 C++函数例子工程中包含的三个文件的源代码如下:

//C++头文件cppExample.h #ifndef CPP_EXAMPLE_H #define CPP_EXAMPLE_H extern "C" int add( int x, int y ); #endif //C++实现文件cppExample.cpp #i nclude "cppExample.h" int add( int x, int y ) {    return x + y; } /* C 实现文件cFile.c /* 这样会编译出错:#i nclude "cExample.h" */ extern int add( int x, int y ); int main( int argc, char* argv[] ) {    add( 2, 3 );    return 0; }


0 0
原创粉丝点击