摘要

概述了C++的核心概念与实用技巧,从变量定义和常量处理(如用const替代#define)到函数设计(内联函数、缺省参数、重载)以及内存管理(new/delete、malloc/free)、引用与指针等。

目录

[TOC]

结构、联合、枚举名称直接做类型名

C++在中定义变量时说明为:

1
2
3
4
5
bool done;

string str;

number x;

C中必须写成:

1
2
3
4
5
enum boole done;

struct string str;

union number x;

使用const代替#define

在C中习惯使用#define来定义常量, 例如 :
#define N 100
实际上这种方法只是在预编译时进行了字符置换, 把程序中出现的标识符N 全部置换成100. 在预编译之后, 程序中不再有N这个标识符. N不是变量,没有类型, 不占存储单元, 且易出错 .
C++中提供了一种更灵活, 更安全的方式来定义常量, 即使用const修饰符来定义常量, 例如 :
const int N = 100;
这个常量是有类型, 占用存储单元有地址, 可以用指针指向它, 但不能改变它
const相比#define消除了不安全性, 就让我们举例说明 .
例如以下代码

1
2
3
4
int a = 1;
#define T1 a+a
#define T2 T1-T1
cout << "T2= " << T2 << endl;

我们会认为输出为T2= 0
但实际上输出为T2= 2
其原因是C++把cout << "T2= " << T2 << endl ; 解释成了cout<<"T2= " << a+a-a+a << endl ;
但如果换成const就不会引起此错误, 例如 :

1
2
3
4
int a = 1;
const int t1 = a + a;
const int t2 = t1 - t1;
cout << "t2= " << t2 << endl;

输出就为t2= 0
具体实现如下 :

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
int a = 1;
#define T1 a+a
#define T2 T1-T1
const int t1 = a + a;
const int t2 = t1 - t1;
int main() {
cout << "T2= " << T2 << endl;
cout << "t2= " << t2 << endl;
system("pause");
return 0;
}

输出:

1
2
T2=2
t2= 0

const的其他用法

常量引用

初始化常量引用时允许用任意表达式作为初始值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int i = 42;  
const int &r1 = i; //正确:允许将const int & 绑定到一个普通int对象上
const int &r2 = 42; //正确
const int &r3 = r1 * 2; //正确
int &r4 = r1 * 2; //错误

double dval = 3.14;
const int &ri = dval; //正确
//等价于
const int temp = dval;
const int &ri = temp;

int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; //正确
r2 = 0; //错误

常量指针

定义: 又叫常指针,可以理解为常量的指针,也即这个是指针,但指向的是个常量,这个常量是指针的值(地址),而不是地址指向的值。

关键点:

  • 1.常量指针指向的对象不能通过这个指针来修改,可是仍然可以通过原来的声明修改;
  • 2.常量指针可以被赋值为变量的地址,之所以叫常量指针,是限制了通过这个指针修改变量的值;
  • 3.指针还可以指向别处,因为指针本身只是个变量,可以指向任意地址;

 

代码形式:

1
int const* p;  const int* p;

指针常量

定义:本质是一个常量,而用指针修饰它。指针常量的值是指针,这个值因为是常量,所以不能被赋值。

关键点:

1.它是个常量!

2.指针所保存的地址可以改变,然而指针所指向的值却不可以改变;

3.指针本身是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化;

代码形式:

1
int* const p;

指向常量的常指针

定义:指向常量的指针常量就是一个常量,且它指向的对象也是一个常量。

关键点:

1.一个指针常量,指向的是一个指针对象;

2.它指向的指针对象且是一个常量,即它指向的对象不能变化;

代码形式:

1
const int* const p;

那如何区分这几类呢? 带两个const的肯定是指向常量的常指针,很容易理解,主要是如何区分常量指针和指针常量:

一种方式是看 * 和 const 的排列顺序,比如

1
2
3
int const* p;    //const * 即常量指针
const int* p; //const * 即常量指针
int* const p; //* const 即指针常量

还一种方式是看const离谁近,即从右往左看,比如

1
2
3
int const* p;    //const修饰的是*p,即*p的内容不可通过p改变,但p不是const,p可以修改,*p不可修改;
const int* p; //同上
int* const p; //const修饰的是p,p是指针,p指向的地址不能修改,p不能修改,但*p可以修改;

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//-------常量指针-------
const int *p1 = &a;
a = 300; //OK,仍然可以通过原来的声明修改值,
//*p1 = 56; //Error,*p1是const int的,不可修改,即常量指针不可修改其指向地址
p1 = &b; //OK,指针还可以指向别处,因为指针只是个变量,可以随意指向;

//-------指针常量-------//
int* const p2 = &a;
a = 500; //OK,仍然可以通过原来的声明修改值,
*p2 = 400; //OK,指针是常量,指向的地址不可以变化,但是指向的地址所对应的内容可以变化
//p2 = &b; //Error,因为p2是const 指针,因此不能改变p2指向的内容

//-------指向常量的常量指针-------//
const int* const p3 = &a;
//*p3 = 1; //Error
//p3 = &b; //Error
a = 5000; //OK,仍然可以通过原来的声明修改值

在实际应用中,常量指针要比指针常量用的多,比如常量指针经常用在函数传参中,以避免函数内部修改内容。

1
2
3
4
5
size_t strlen(const char* src); //常量指针,src的值不可改变;
char a[] = "hello";
char b[] = "world";
size_t a1 = strlen(a);
size_t b1 = strlen(b);

虽然a、b是可以修改的,但是可以保证在strlen函数内部不会修改a、b的内容。

内联函数的使用

C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。

对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。

如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。

在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。

下面是一个实例,使用内联函数来返回两个数中的最大值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

using namespace std;

inline int Max(int x, int y)
{
return (x > y)? x : y;
}

// 程序的主函数
int main( )
{

cout << "Max (20,10): " << Max(20,10) << endl;
cout << "Max (0,200): " << Max(0,200) << endl;
cout << "Max (100,1010): " << Max(100,1010) << endl;
return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
Max (20,10): 20
Max (0,200): 200
Max (100,1010): 1010

带有缺省参数值的函数

定义

c++中,定义函数的时候可以让最右边的连续若干个参数有缺省值,在调用函数的时候,如果不写相应位置的参数,则调用的参数就为缺省值。

例如:

1
2
3
void fun(int a, int b = 1, int c = 2) {
cout << "a=" << a << "\tb=" << b << "\tc=" << c << endl;
}

在调用时,如果参数bc的参数没有给出,则默认为缺省值。

函数缺省参数的作用在于提高程序的可扩充性。比如某个以及写好的函数需要添加新的参数,而原先调用函数的的那些语句未必需要新增加的参数,为了避免对原来所有调用该函数的地方进行修改,就可以使用函数缺省参数了。

全缺省

顾名思义,全缺省就是参数的所有值都为缺省参数,如下代码所示:

1
2
3
4
5
6
7
8
9
10
#include<iostream>
using namespace std;
void fun(int a=1, int b = 2, int c = 3) {
cout << "a=" << a << "\tb=" << b << "\tc=" << c << endl;
}
int main() {
fun();
fun(4, 5, 6);
return 0;
}

执行该程序,输出:

1
2
a=1     b=2     c=3
a=4 b=5 c=6

需要注意的是,我们在调用函数时,只能缺省最右边的若干个参数,形如:fun(4, , 6);这种调用是错误的调用方法。

半缺省

半缺省指的是参数中有一部分为缺省参数,有一部分为非缺省参数。

值得注意的是,缺省参数只能为最右边的若干个

例如:

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
using namespace std;
void fun(int a, int b = 2, int c = 3) {
cout << "a=" << a << "\tb=" << b << "\tc=" << c << endl;
}
int main() {
fun(1);
fun(1, 4);
fun(4, 5, 6);
return 0;
}

执行上面程序,输出如下:

1
2
3
a=1     b=2     c=3
a=1 b=4 c=3
a=4 b=5 c=6

形如:void fun(int a=1, int b, int c = 3) { }这样的语句是错误的用法。

形如:fun(1, ,3)这种调用也是错误的。

总之记住,缺省参数只能为最右边的若干个参数。

函数重载

多个函数可以同名只要函数参数的类型、个数不同。

在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。

下面的实例中,同名函数 print() 被用于输出不同的数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class printData
{
public:
void print(int i) {
cout << "整数为: " << i << endl;
}

void print(double f) {
cout << "浮点数为: " << f << endl;
}

void print(char c[]) {
cout << "字符串为: " << c << endl;
}
};

int main(void)
{
printData pd;

// 输出整数
pd.print(5);
// 输出浮点数
pd.print(500.263);
// 输出字符串
char c[] = "Hello C++";
pd.print(c);

return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
整数为: 5
浮点数为: 500.263
字符串为: Hello C++

内存的分配与释放

new 和 delete 是 C++ 用于管理堆内存的两个运算符,对应于C语言中的 malloc 和 free,但是 malloc 和 free 是函数,而new 和 delete 是运算符。除此之外,new 在申请内存的同时,还会调用对象的构造函数,而 malloc 只会申请内存;同样,delete 在释放内存之前,会调用对象的析构函数,而 free 只会释放内存。

C++

new运算符申请内存:

将调用相应的 operator new(size_t) 函数动态分配内存,在分配到的动态内存块上 初始化 相应类型的对象(构造函数)并返回其首地址。如果调用构造函数初始化对象时抛出异常,则自动调用 operator delete(void*, void*) 函数释放已经分配到的内存。

delete运算符释放内存:

调用相应类型的析构函数,处理类内部可能涉及的资源释放,调用相应的 operator delete(void *) 函数。

1
2
3
4
5
6
int main()
{
T * t = new T(); // 先内存分配,再构造函数
delete t; // 先析构函数,再内存释放
return 0;
}

new表达式

1
2
3
4
5
6
7
8
9
10
11
12
type  * p_var = new type;   
//分配内存,但未初始化
int * a = new int;

type * p_var = new type(init);
//分配内存时,将 *a 初始化为 8
int * a = new int(8);

type *p_var = new type [size];
//分配了3个int大小的连续内存块,但未初始化
int * a = new int[3] ;
1234567891011

delete表达式

1
2
3
4
5
6
7
8
删除单变量地址空间
int *a = new int;
delete a;//释放单个int的空间

删除数组空间
int *a = new int[5];
delete []a;//释放int数组空间
1234567

C

内存区域可以分为栈,堆,静态存储区和常量存储区。局部变量,函数形参,临时变量都是在栈上获得内存的,它们获取的方式都是由编译器自动执行的。

而C标准函数库提供了许多函数来实现对堆上内存管理,其中包括:malloc函数,free函数,calloc函数和realloc函数。使用这些函数需要包含头文件stdlib.h。

(1)malloc函数

malloc函数可以从堆上获得指定字节的内存空间,其函数声明如下:

1
void * malloc(int n);

其中,形参n为要求分配的字节数。如果函数执行成功,malloc返回获得内存空间的首地址;如果函数执行失败,那么返回值为NULL。由于malloc函数值的类型为void型指针,因此,可以将其值类型转换后赋给任意类型指针,这样就可以通过操作该类型指针来操作从堆上获得的内存空间。

需要注意的是,malloc函数分配得到的内存空间是未初始化的。因此,一般在使用该内存空间时,要调用另一个函数memset来将其初始化为全0。memset函数的声明如下:

1
void * memset (void * p,int c,int n) ;

该函数可以将指定的内存空间按字节单位置为指定的字符c。其中,p为要清零的内存空间的首地址,c为要设定的值,n为被操作的内存空间的字节长度。

1
2
3
4
5
6
int * p=NULL;
p=(int *)malloc(sizeof(int));
if(p==NULL){
printf(“Can’t get memory!\n”);
}
memset(p,0,siezeof(int));

(2)free函数

从堆上获得的内存空间在程序结束以后,系统不会将其自动释放,需要程序员来自己管理。一个程序结束时,必须保证所有从堆上获得的内存空间已被安全释放,否则,会导致内存泄露。

1
void free (void * p);

由于形参为void指针,free函数可以接受任意类型的指针实参。

但是,free函数只是释放指针指向的内容,而该指针仍然指向原来指向的地方,此时,指针为野指针,如果此时操作该指针会导致不可预期的错误。安全做法是:在使用free函数释放指针指向的空间之后,将指针的值置为NULL。

1
2
3
free(p);
p=NULL;
//注:使用malloc函数分配的堆空间在程序结束之前必须释放

(3)calloc函数

calloc函数的功能与malloc函数的功能相似,都是从堆分配内存。其函数声明如下:

1
void *calloc(int n,int size);

函数返回值为void型指针。如果执行成功,函数从堆上获得size X n的字节空间,并返回该空间的首地址。如果执行失败,函数返回NULL。该函数与malloc函数的一个显著不同时是,calloc函数得到的内存空间是经过初始化的,其内容全为0。calloc函数适合为数组申请空间,可以将size设置为数组元素的空间长度,将n设置为数组的容量。

1
2
3
4
5
6
7
8
9
10
11
12
 int * p=NULL;
//为p从堆上分配SIZE个int型空间

p=(int *)calloc(SIZE,sizeof(int));

if(NULL==p){
printf("Error in calloc.\n");
return -1;
}

free(p);
p = NULL;

(4)realloc函数

realloc函数的功能比malloc函数和calloc函数的功能更为丰富,可以实现内存分配和内存释放的功能,其函数声明如下:

1
void * realloc(void * p,int n);

其中,指针p必须为指向堆内存空间的指针,即由malloc函数、calloc函数或realloc函数分配空间的指针。realloc函数将指针p指向的内存块的大小改变为n字节。如果n小于或等于p之前指向的空间大小,那么。保持原有状态不变。如果n大于原来p之前指向的空间大小,那么,系统将重新为p从堆上分配一块大小为n的内存空间,同时,将原来指向空间的内容依次复制到新的内存空间上,p之前指向的空间被释放。relloc函数分配的空间也是未初始化的。

1
2
3
4
5
6
7
8
 int * p=NULL;
p=(int *)malloc(sizeof(int));

p=(int *)realloc(p,3*sizeof(int));

//释放p指向的空间
realloc(p,0);
p=NULL;

注:使用malloc函数,calloc函数和realloc函数分配的内存空间都要使用free函数或指针参数为NULL的realloc函数来释放。

C++ 引用

引用变量是一个别名,也就是说,它是某个已存在变量的另一个名字。一旦把引用初始化为某个变量,就可以使用该引用名称或变量名称来指向变量。

C++ 引用 与 指针

引用很容易与指针混淆,它们之间有三个主要的不同:

  • 不存在空引用。引用必须连接到一块合法的内存。
  • 一旦引用被初始化为一个对象,就不能被指向到另一个对象。指针可以在任何时候指向到另一个对象。
  • 引用必须在创建时被初始化。指针可以在任何时间被初始化。

C++ 中创建引用

试想变量名称是变量附属在内存位置中的标签,您可以把引用当成是变量附属在内存位置中的第二个标签。因此,您可以通过原始变量名称或引用来访问变量的内容。例如:

1
int i = 17;

我们可以为 i 声明引用变量,如下所示:

1
2
int&  r = i;
double& s = d;

在这些声明中,& 读作引用。因此,第一个声明可以读作 “r 是一个初始化为 i 的整型引用”,第二个声明可以读作 “s 是一个初始化为 d 的 double 型引用”。下面的实例使用了 int 和 double 引用:

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

using namespace std;

int main ()
{
// 声明简单的变量
int i;
double d;

// 声明引用变量
int& r = i;
double& s = d;

i = 5;
cout << "Value of i : " << i << endl;
cout << "Value of i reference : " << r << endl;

d = 11.7;
cout << "Value of d : " << d << endl;
cout << "Value of d reference : " << s << endl;

return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

1
2
3
4
Value of i : 5
Value of i reference : 5
Value of d : 11.7
Value of d reference : 11.7

引用通常用于函数参数列表和函数返回值。下面列出了 C++ 程序员必须清楚的两个与 C++ 引用相关的重要概念:

概念 描述
把引用作为参数 C++ 支持把引用作为参数传给函数,这比传一般的参数更安全。
把引用作为返回值 可以从 C++ 函数中返回引用,就像返回其他数据类型一样。

void指针

void* 是一种特殊的指针类型,可用于存放任意对象的地址。一个 void* 指针存放着一个地址,这一点和其他指针类似。

在介绍 void 指针前,简单说一下 void 关键字使用规则:

  • 如果函数没有返回值,那么应声明为 void 类型;
  • 如果函数无参数,那么应声明其参数为 void;(常省略)
  • 如果函数的参数或返回值可以是任意类型指针,那么应声明其类型为 void*
  • void 的字面意思是“无类型”,void*则为“无类型指针”,void不能代表一个真实的变量,void体现了一种抽象。

(1)任何类型的指针都可以直接赋值给void指针, 且无需进行强制类型转换。

任何类型指针都可以直接赋值给void指针。

1
2
3
4
double obj = 3.14, *pd = &obj;
void* pv = &obj; // 正确,void* 能存放任意类型对象的地址
// obj 可以是任意类型的对象
pv = pd; // 正确,pv 可以存放任意类型的指针

(2)void指针并不能无需类型转换直接赋值给其他类型

如果要把 void 类型的指针赋值给其他类型的指针,需要进行显式转换。

1
2
3
4
5
6
double obj = 3.14, *pd = &obj;
void *pv = &obj;

double *pd1 = pv; // 错误,不可以直接赋值
double *pd2 = (double*)pv; // 必须进行显示类型转换
cout << *pd2 << endl; // 3.14

(3)void指针可以直接和其他类型的指针进行比较指针存放的地址值是否相同

1
2
3
4
5
6
7
double obj = 3.14, *pd = &obj;
void *pv = &obj;

double *pd1 = pv; // 错误,不可以直接赋值
double *pd2 = (double*)pv; // 必须进行显示类型转换
cout << *pd2 << endl; // 3.14
cout << (pv == pd2) << endl; // 1

(4)void指针只有强制类型转换后才可以正常对其操作

我们对该地址中到底是个什么类型的对象并不了解,因此不能直接操作 void* 指针所指的对象,也就无法确定能在这个对象上做哪些操作。

概括来说,以 void* 的视角来看内存空间也就是仅仅是内存空间,没办法访问内存空间中所存的对象,因此只有对其进行恰当的类型转换之后才可以对其进行相应的访问。

也就是说一个 void 指针必须要经过强制类型转换以后才有意义。

1
2
3
4
double obj = 3.14, *pd = &obj;
void *pv = &obj;

cout << *(double*)pv << endl; // 3.14

(5)void指针变量和普通指针一样可以通过 NULL 或 nullptr 来初始化,表示一个空指针

1
2
3
4
void *pv = 0; 
void *pv2 = NULL;
cout << pv << endl; // 值为0x0
cout << pv2<< endl; // 值为0x0

(6)当void指针作为函数的输入和输出时,表示可以接受任意类型的输入指针和输出任意类型的指针

如果函数的参数或返回值可以是任意类型指针,那么应声明其类型为void*

在函数调用过程中的使用 void 指针作为输入输出参数也非常好用,可以灵活使用任意类型的指针,避免只能使用固定类型的指针。

作用域标示符::

通常情况下,如果有两个同名变量,一个是全局的,另一个是局部的,那么局部变量在其作用域内具有较高的优先权。

下面的例子说明了这个问题。

1
2
3
4
5
6
7
8
9
#include "iostream.h"
int avar=10;
main( )
{
int avar;
avar=25;
cout<<"avar is"<<avar<<endl;
return 0;
}

如果希望在局部变量的作用域内使用同名的全局变量,可以在全局变量加上“::”,此时::avar代表全局变量avar

1
2
3
4
5
6
7
8
9
10
#include <iostream.h>
int avar=10;
main()
{
int avar;
avar=25;
cout<<"local avar ="<<avar<<endl;
cout<<"global avar="<<::avar<<endl;
return 0;
}

强制类型转换

在C中如果要把一个整数(int)转换为浮点数(float), 要求使用如下的格式:

1
2
int i=10; 
float x=(float)i;

C++支持这样的格式,但上面的语句可改写成:

1
int  i=10; float x=float(i);

以上两种方法C++都接受,但推荐使用后一种方式。

无名联合

无名联合是C++中的一种特殊联合,可以声明一组无标记名共享同一段内存地址的数据项。如:

1
2
3
4
5
union
{
int i;
float f;
}

在此无名联合中,声明了变量i和f具有相同的存储地址。无名联合可通过使用其中数据项名字直接存取,例如可以直接使用上面的变量i或f,如:i=20;