标签:const 空间 输入参数 sys var ref 输入 对象引用 局限
(1)srand(time(0))和rand();前者是为产生随机数提供种子函数,后者就是随机值产生的函数;而套用的time(0),目的是为了 产生随着时间改变的随机数。
(2)这两个函数分别属于两个头文件 #include <stdlib.h>、#include<time.h>
(1) 一个对象作为函数参数,以值传递的方式传入函数体;
(2)一个对象作为函数返回值,以值传递的方式从函数返回;
(3)一个对象用于给另外一个对象进行初始化(常称为赋值初始化);
(1) extern修饰变量的声明。
如果文件a.c需要引用b.c中变量int v,就可以在a.c中声明extern int v,然后就可以引用变量v。
这里需要注意的是,被引用的变量v的链接属性必须是外链接(external)的,也就是说a.c要引用到v,不只是取决于在a.c中声明extern int v,还取决于变量v本身是能够被引用到的。
这涉及到c语言的另外一个话题--变量的作用域。能够被其他模块以extern修饰符引用到的变量通常是全局变量。
还有很重要的一点是,extern int v可以放在a.c中的任何地方,比如你可以在a.c中的函数fun定义的开头处声明extern int v,然后就可以引用到变量v了,只不过这样只能在函数fun作用域中引用v罢了,这还是变量作用域的问题。对于这一点来说,很多人使用的时候都心存顾虑。好像extern声明只能用于文件作用域似的。
(2) extern修饰函数声明。
从本质上来讲,变量和函数没有区别。函数名是指向函数二进制块开头处的指针。
如果文件a.c需要引用b.c中的函数,比如在b.c中原型是int fun(int mu),那么就可以在a.c中声明extern int fun(int mu),然后就能使用fun来做任何事情。
就像变量的声明一样,extern int fun(int mu)可以放在a.c中任何地方,而不一定非要放在a.c的文件作用域的范围中。
对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件。使用extern和包含头文件来引用函数有什么区别呢?extern的引用方式比包含头文件要简洁得多!extern的使用方法是直接了当的,想引用哪个函数就用extern声明哪个函数。
这样做的一个明显的好处是,会加速程序的编译(确切的说是预处理)的过程,节省时间。在大型C程序编译过程中,这种差异是非常明显的。
(3)此外,extern修饰符可用于指示C或者C++函数的调用规范。
比如在C++中调用C库函数,就需要在C++程序中用extern “C”声明要引用的函数。
这是给链接器用的,告诉链接器在链接的时候用C函数规范来链接。主要原因是C++和C程序编译完成后在目标代码中命名规则不同。
(1)、 局部静态变量
局部变量按照存储形式可以分为三种,分别是auto、static、register。
与auto类型(普通)局部变量相比,static有三点不同:
1. 存储空间分配不同
auto类型分配在栈上,属于动态存储类别,占动态存储空间,函数调用结束后自动释放;static类型分配在静态存储区,在程序整个运行期间都不释放;
两者作用域相同,但是生存期不同。
2. static局部变量在初次运行时进行初始化工作,且只初始化一次。
3. 对于局部静态变量,如果不赋初值,编译期会自动赋初值0或者空;
auto类型的初值是不确定的。
对于C++的类对象例外,class的对象实例如果不初始化,则会自动调用默认构造函数,不管是不是static类型。
特点:static局部变量的“记忆性”与生存期的“全局性”
所谓“记忆性”是指在两次函数调用时,在第二次调用进入时,能保持第一次调用退出时的值。
#include <iostream> using namespace std; void staticLocalVar() { static int a = 0; cout<<"a="<<++a<<endl; } int main() { staticLocalVar(); // a=1 staticLocalVar(); // a=2 system("pause"); return 0; }
利用生存期的”全局性“改善return a pointer / reference to a local object的问题,local object的问题在于退出函数时,生存期就结束,局部变量就会被销毁;利用static就可以延长局部变量的生存期。
// IP address to string format // Used in Ethernet Frame and IP Header analysis const char * IpToStr(UINT32 IpAddr) { static char strBuff[16]; // static局部变量, 用于返回地址有效 const unsigned char *pChIP = (const unsigned char *)&IpAddr; sprintf(strBuff, "%u.%u.%u.%u", pChIP[0], pChIP[1], pChIP[2], pChIP[3]); return strBuff; }
注意事项:
1. “记忆性”是程序运行很重要的一点就是可重复性,而static变量的“记忆性”破坏了可重复性,造成不同时刻同一函数的运行结果不同。
2. “生存期”全局性和唯一性。 普通的局部变量在栈上分配空间,因此每次调用函数时,分配的空间都可能不一样,而static具有全局唯一性的特点,每次调用时都指向同一块内存,这就造成一个很重要的问题---不可重入性!!!
在多线程或者递归程序中要特别注意。
(2) 外部静态变量/函数
在C中static的第二种含义:用来表示不能被其它文件访问的全局变量和函数。
此处static的含义是指对函数的作用域仅仅局限于本文件(所以又称为内部函数)。
注意:对于外部(全局)变量,不论是否有static限制,它的存储区域都是在静态存储区,生存期都是全局的,此时的static只是起作用域限制作用,限制作用域在本文件内部。
使用内部函数的好处是:不同的人编写不同的函数时,不用担心函数同名问题。
//file1.cpp static int varA; int varB; extern void funA() { } static void funB() { } //file2.cpp extern int varB; // 使用file1.cpp中定义的全局变量 extern int varA; // 错误! varA是static类型, 无法在其他文件中使用 extern void funA(); // 使用file1.cpp中定义的函数 extern void funB(); // 错误! 无法使用file1.cpp文件中static函数
(3)静态数据成员/成员函数(C++特有)
C++重用了这个关键字,它表示属于一个类而不是属于此类的任何特定的对象的变量和函数。
静态类成员包括静态数据成员和静态函数成员。
1. 静态数据成员
类体中的数据成员的声明前加上static关键字,该数据成员就成为了该类的静态数据成员。和其他数据成员一样,静态数据成员也遵守public/protected/private访问规则。同时静态数据成员还具有以下特点。
1) 静态数据成员的定义
静态数据成员实际上是类域中的全局变量。所以,静态数据成员的定义(初始化)不应该被放在头文件中。其定义方式与全局变量相同。举例如下:
xxx.h文件 class base { private: static const int _i; //声明,标准c++支持有序类型在类体中初始化,但vc6不支持。 }; xxx.cpp文件 const int base::_i = 10; //定义(初始化)时不受private和protected访问限制.
注:不要试图在头文件中定义(初始化)静态数据成员。在大多数情况下,这会引起重复定义。即使加上#ifndef #define #endif或者#pragma once也不行。
2) 静态数据成员被类的所有对象所共享,包括该类的派生类的对象。
#include <iostream> using namespace std; class base { public: static int _num; //声明 }; int base::_num = 0; //静态数据成员的真正定义 class derived : public base { }; int main() { base a; derived b; a._num++; cout<<"base class static data number _num is "<<a._num<<endl; // 1 b._num++; cout<<"derived class static data number _num is "<<b._num<<endl;// 2 system("pause"); return 0; }
3) 静态数据成员可以成为成员函数的可选参数,而普通数据成员则不可以。
class base { public: static int _staticVar; int _var; void foo1(int i = _staticVar);//正确,_staticVar为静态数据成员 void foo2(int i = _var);//错误,_var为普通数据成员 };
4 ) ★静态数据成员的类型可以是所属类的类型,而普通数据成员则不可以。普通数据成员的只能声明为所属类类型的指针或引用。举例如下:
class base { public: static base _object1;//正确,静态数据成员 base object2;//错误 base *pObject;//正确,指针 base &mObject;//正确,引用 };
5 ).★这个特性,我不知道是属于标准c++中的特性,还是vc6自己的特性。
静态数据成员的值在const成员函数中可以被合法的改变。举例如下:
class base { public: base() { _i = 0; _val = 0; } mutable int _i; static int _staticVal; int _val; void test() const { _i++;//正确,mutable数据成员 _staticVal++;//正确,static数据成员 _val++;//错误 } }; int base::_staticVal = 0;
2 静态成员函数
1).静态成员函数的地址可用普通函数指针储存,而普通成员函数地址需要用类成员函数指针来储存。举例如下:
class base { static int func1(); int func2(); }; int (*pf1)() = &base::func1; //普通的函数指针 int (base::*pf2)() = &base::func2; //成员函数指针
2).静态成员函数不可以调用类的非静态成员。因为静态成员函数不含this指针。
3).静态成员函数不可以同时声明为 virtual、const、volatile函数。举例如下:
class base { virtual static void func1();//错误 static void func2() const;//错误 static void func3() volatile;//错误 };
最后要说的一点是,静态成员是可以独立访问的,也就是说,无须创建任何对象实例就可以访问。
(1) const
的基本功能与用法
1).将限定符声明为只读
使用方法如下,在类型前/后加上关键字const
,该变量必须被初始化,否则编译错误;该变量不能被重新赋值,否则也编译错误。
const int i = 50; // 编译正确 const int j; // 编译错误 int k = 0; i = k; // 编译错误 k = i; // 编译正确
2).用于修饰函数形参,保护参数使其不被修改
用法1:若形参为const A* a
,则不能改变函数所传递的指针内容,这样对指针所指向的内容起到保护作用,这里需要注意的是,该修饰不能改变指针所指向地址所存储的内容,但是指针a所指向的地址可以被改变,具体例子如下:
void Test(const int *a) { *a = 1; //错误,*a不能被赋值 a = new int(10086); //正确,为指针a开辟新的空间,并令*a=10086 } int main() { int *a = new int(10000); Test(a); return 0; }
用法2:若形参为const A& a
,则不能改变函数传递进来的引用对象,从而保护了原对象的属性。
对于自定义的数据类型,用引用传递速度较快,如果不想改变原值,就可以用const
来保护参数,如以下例子:
void Test(const int &a) //保护L7中的a不会被改变 { a = 2;//错误,a不能给常量赋值 } int main() { int a = 3; Test(a); return 0; }
事实上对于内置型数据类型(如以上例子中的int类型),用引用传递不会使速度更快。如果是用引用传参,一般是为了改变参数值;如果不想改变参数的值,直接值传递即可,不必使用const修饰。而对于自定义数据类型的输入参数,为了提高速度和效率,应使用“const + 引用传递”代替值传递。例如:
将函数 void Test(A a) 改为-> void Test(const A &a)
3).用于修饰函数返回值
用法1:用const
修饰返回值为对象本身(非引用和指针)的情况多用于二目操作符重载函数并产生新对象的时候 。
举例:
const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); } Rational a,b; Radional c; (a*b) = c;//错误
用法2:不建议用const修饰函数的返回值类型为某个对象或对某个对象引用的情况。原因如下:如果返回值为某个对象为const(const A test = A 实例)或某个对象的引用为const(const A& test = A实例) ,则返回值具有const属性,则返回实例只能访问类A中的公有(保护)数据成员和const成员函数,并且不允许对其进行赋值操作,这在一般情况下很少用到,具体例子如下:
class A { public: int y; A(int y):x(x),y(y){}; void Sety(int y){this->y = y;} }; const A Test1(A a) { return a; } const A& Test2(A &a) { return a; } int main() { A a(2); Test1(a).Sety(3);//错误,因为Test1(a)的返回值是个const,不能被Sety(3)修改 Test2(a).Sety(3);//错误,因为Test1(a)的返回值是个const,不能被Sety(3)修改 return 0; }
用法3:如果给采用“指针传递”方式的函数返回值加const
修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const
修饰的同类型指针。例子如下:
const char * GetString(void){} int main() { char *str1=GetString();//错误 const char *str2=GetString();//正确 return 0; }
用法4:函数返回值采用“引用传递”的场合不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。例子如下:
class A { // 以下赋值函数的返回值加const修饰,该返回值的内容不允许修改 A &operate = (const A &other); } A a, b, c; // a,b,c为A的对象 a = b = c; // 正确 (a = b) = c; // 错误,a = b的返回值不允许被再赋值
4).在类成员函数的函数体后加关键字const
在类成员函数的函数体后加关键字const,形如:void fun() const; 在函数过程中不会修改数据成员。如果在编写const成员函数时,不慎修改了数据成员,或者调用了其他非const成员函数,编译器将报错,这大大提高了程序的健壮性。
如果不是在类的成员函数,没有任何效果,void fun() const;和void func();是一样的。
5). 在另一连接文件文件中引用常量
方法:在类型前添加关键字extern const
,表示引用的是常量,因此该常量不允许被再次赋值,举例如下:
extern const int i; // 正确 extern const int j = 10; // 错误,常量不可以被再次赋值
(2).const
常量与#define
的区别
1).const
常量有数据类型,而宏常量没有数据类型
宏常量只进行简单的字符替换,没有类型安全检查,并且在字符替换时可能会产生意料不到的错误,如:
#define I = 10 const long &i = 10; // 由于编译器的优化,使得在const long i=10时i不被分配内存 // 而是已10直接代入以后的引用中,以致在以后的代码中没有错误 //一旦你关闭所有优化措施,即使const long i = 10也会引起后面的编译错误。 char h = I; // 正确 char h = i; // 编译警告,可能由于数的截短带来错误赋值
2).使用const可以避免不必要的内存分配
从汇编的角度来看,const
定义常量只是给出了对应的内存地址, 而不是象#define
一样给出的是立即数,所以,const
定义的常量在程序运行过程中只有一份拷贝,而#define
定义的常量在内存中有若干个拷贝。例子如下:
#define k "Hello world!" const char pk[]="Hello world!"; printf(k); // 为k分配了第一次内存 printf(pk); // 为pk一次分配了内存,以后不再分配 printf(k); // 为k分配了第二次内存 printf(pk);
(3). 使用const
的一些注意事项
1).修改const
所修饰的常量值
以下例子中,i
为const
修饰的变量,可以通过对i
进行类型强制转换,将地址赋给一个新的变量,对该新的变量再作修改即可以改变原来const
修饰的常值。
const int i = 0; int *p=(int*)&i; *p = 100;
2).构造函数不能被声明为const
3).const数据成员的初始化只能在类的构造函数的初始化表中进行
class A { public: const int a; A(int x):a(x)//正确 { a = x;//错误 } };
4).在参数中使用const应该使用引用或指针,而不是一般的对象实例
合理利用const在成员函数中的三种用法(参数、返回值、函数),一般来说,不要轻易的将函数的返回值类型定为const;另外,除了重载操作符外一般不要将返回值类型定为对某个对象的const引用。
5).对于使用const修饰来指针的情况
对于以下情况,const放在变量声明符的前后位置效果是一样的,这种情况下不允许对指针a 的内容进行更改操作:
int i; const int *a = &i; int const*a = &i;
但是,如果const
位于星号的左侧,则const
就是用来修饰指针所指向的变量,即该指针指向一个地址,该地址的内容不可变;如果const
位于星号的右侧,const
就是修饰指针本身,即指针本身是常量:
int i; // 以下一行表示a是一个指针,可以任意指向int常量或者int变量 // 它总是把它所指向的目标当作一个int常量 // 也可以写成int const* a const int *a = &i; // 以下一行表示a是一个指针常量, // 初始化的时候必须固定指向一个int变量 // 之后就不能再指向别的地方了 // 但是指针指向的内容可以改变 int *const a = &i;
6).指针本身是常量,而指针所指向的内容不是常量,这种情况下不能对指针本身进行更改操作,如以下例子中a++是错误的:
int *const a = &i; a++; // 错误,a指针本身是常量,不能再指向别的地方
7).当指针本身和指针所指向的内容均为常量时
这种情况下可写为:
const int * const a = &i;
8).const成员函数返回的引用,也是const
#include<iostream> using namespace std; class A { public: int x; void set(int x){this->x = x;} // const成员函数返回的引用也是const,a // 如果把A&前面的const去掉会出错 // 因为返回的是一个const的对象,返回类型却不是const // 返回的内容和返回的类型不符 const A& Test1()const { // 错误。这是const成员函数的特点 x = 2; // 不限于*this。不管返回的是什么,哪怕是一个定义为非const的对象,结果也是一样的 return *this; } }; int main() { A a, b; // 正确,虽然返回的是一个const,却用另一个非const来接收 b = a.Test1(); // 错误,既然是别名,那么别名的类型要与原来的类型相同 A &c = a.Test1(); // 正确虽然在a.Test1()中a不能改变,但是这里已经出了这个成员函数的作用域 a.set(2); // 正确,b接收了a.Test1()返回的数据的内容,但是它不是const b.set(2); // 错误。a.Test1()是一个对象,这个对象是它的返回值 // 虽然没有名字,但是它就是a.Test1()的返回值 // 值是a.Test1()返回的值,类型是a.Test1()返回的类型 a.Test1().set(2); return 0; }
9).mutable将数据声明为可变数据成员
在C++语言中,mutable是使用较少的关键字,它的作用是:如果一个函数被const 修饰,那么它将无法修改其成员变量的,但是如果一个成员变量是被mutable修饰的话,则可以修改。
mutable 可以用来指出,即使成员函数或者类变量为const,其某个成员也可以被修改。反过来说,可变数据成员永远不能成为const,即使它是const对象的成员。
class A { public: int x; mutable int y; A(int a, int b):x(a),y(b){} }; int main() { const A a(0, 0); // const对象必须初始化 a.x = 1; // 错误 a.y = 2; // 正确,mutable修饰使得成员可被修改,即使对象a为const return 0; }
先来一段代码:
#include "stdio.h" #include "conio.h" class Parent { public: char data[20]; void Function1(); virtual void Function2(); // 这里声明Function2是虚函数 }parent; void Parent::Function1() { printf("This is parent,function1\n"); } void Parent::Function2() { printf("This is parent,function2\n"); } class Child:public Parent { void Function1(); void Function2(); } child; void Child::Function1() { printf("This is child,function1\n"); } void Child::Function2() { printf("This is child,function2\n"); } int main(int argc, char* argv[]) { Parent *p; // 定义一个基类指针 if(_getch()==‘c‘) // 如果输入一个小写字母c p=&child; // 指向继承类对象 else p=&parent; // 否则指向基类对象 p->Function1(); // 这里在编译时会直接给出Parent::Function1()的入口地址。 p->Function2(); // 注意这里,执行的是哪一个Function2? return 0; }
用任意版本的Visual C++或Borland C++编译并运行,输入一个小写字母c,得到下面的结果:
This is parent,function1
This is child,function2
为什么会有第一行的结果呢?因为我们是用一个Parent类的指针调用函数Fuction1(),虽然实际上这个指针指向的是Child类的对象,但编译器无法知道这一事实(直到运行的时候,程序才可以根据用户的输入判断出指针指向的对象),它只能按照调用Parent类的函数来理解并编译,所以我们看到了第一行的结果。 那么第二行的结果又是怎么回事呢?我们注意到,Function2()函数在基类中被virtual关键字修饰,也就是说,它是一个虚函数。虚函数最关键的特点是“动态联编”,它可以在运行时判断指针指向的对象,并自动调用相应的函数。
如果我们在运行上面的程序时任意输入一个非c的字符,结果如下:
This is parent,function1
This is parent,function2
请注意看第二行,它的结果出现了变化。程序中仅仅调用了一个Function2()函数,却可以根据用户的输入自动决定到底调用基类中的Function2还是继承类中的Function2,这就是虚函数的作用。我们知道,在MFC中,很多类都是需要你继承的,它们的成员函数很多都要重载,比如编写MFC应用程序最常用的CView::OnDraw(CDC*)函数,就必须重载使用。把它定义为虚函数(实际上,在MFC中OnDraw不仅是虚函数,还是纯虚函数),可以保证时刻调用的是用户自己编写的OnDraw。虚函数的重要用途在这里可见一斑。
PS:一定要注意“静态联翩 ”和“ 动态联编 ”的区别,对于我来说,若没有在VC6.0中亲自去测试,凭自己的感觉,当在键盘中输入“c”时,我会觉得由于有:p=&child;这句代码,我认为结果会是:
This is child,function1 This is child,function2
但是实际结果却是:
This is parent,function1 This is child,function2
因为虽然实际上这个指针指向的是Child类的对象,但编译器无法知道这一事实,它只能按照调用Parent类的函数来理解并编译,所以我们看到了第一行的结果。
第二行中调用了子类的function2,完全是因为virtual 的功能,virtual实现了动态联编,它可以在运行时判断指针指向的对象,并自动调用相应的函数。当然,如果执行的是:p=&parent; 这一句,该指针很明显的是指向父类,那么肯定调用的是父类的方法。
标签:const 空间 输入参数 sys var ref 输入 对象引用 局限
原文地址:https://www.cnblogs.com/lzy820260594/p/11315400.html