先记录一些零碎的内容
C++为类型建立别名的方式有两种:
预处理器:在编译时用char替换所有的BYTE,从而使BYTE成为char的别名
#define BYTE char
#define BYTE_POINTER char*
BYTE_POINTER p1, p2;
// char *p1, p2;
typedef:可以处理更复杂的类型的别名,不会出现上面pointer的问题,typedef不会创建新类型,知识为已有的类型建立一个新的名称
typedef typeName aliasName;
typedef char BYTE;
typedef char * BYTE_POINTER;
1、函数参数与按值传递
C++通常按值传递函数参数:函数被调用时,会创建一个新的变量,并将其初始化,这样就不会影响到外部的数据。用于接收传递值的变量被称为形参(parameter),传递给函数的值被称为实参(argument)
2、给函数传递一维数组
将数组作为参数意味着将数组的位置、元素的类型提交给函数,此时函数仍按值传递,只不过是传递的一个地址,所以传递数组时要附加数组长度。
int sum_arr(int arr[], int n)
{
cout << sizeof(arr) << endl; // 4
int sum = 0;
for (int i = 0; i < n; i++)
{
sum += arr[i];
arr[i] += 1;
}
return sum;
}
int arr[]{ 1,2,3,4,5 };
int sum = sum_arr(arr, sizeof(arr));
for (size_t i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
cout << arr[i] << " "; // 2 3 4 5 6
由于实际传递的是一个地址,在函数内部对数组使用sizeof返回的是指针的大小,此外在函数内修改数组内元素的值时,会影响到外部的实参。
如果我们并不想函数能够修改我们传递进去的内容,那么我们可以用const来修饰形参:
int sum_arr2(const int *arr, int n)
{
cout << sizeof(arr) << endl; // 4
int sum = 0;
for (int i = 0; i < n; i++)
{
sum += arr[i];
// arr[i] += 1; // error
}
return sum;
}
3、const修饰符
可以使用两种不同的方式将const用于指针:
a. 第一种方法是将指针指向一个常量对象,这样可以防止使用指针来修改所指向的值。
int n = 10;
const int *pn = &n;
cout << n << " " << *pn << endl; // 10 10
n = 20;
cout << n << " " << *pn << endl; // 20 20
但是,pn声明为const指针不意味着它指向的值是一个常量,也就是n是可以修改的,只是不能通过pn来修改。
C++可以将常规指针变量的地址赋给指向const的指针,可以将const变量的地址赋给指向const的指针,但是禁止将const地址赋给非const指针(除非使用const_cast)
const int cn = 10;
const int *pcn = &cn;
//int *pcn2 = &cn; // invalid
b. 第二种是将指针本身声明为常量,防止修改指针指向的位置,允许使用指针修改指向位置的值
int n = 10, n2 = 20;
int * const pcn = &n;
// pcn = &n2; // error
*pcn = 11;
cout << n; // 11
4、传递二维数组
int arr[][4]{ {1,2,3,4}, {5,6,7,8} };
int total = total_arr(arr, 2);
如何将一个二维数组传递给函数呢?total_arr的第一个参数的类型:一个指向由4个int组成的数组的指针,即数组指针,数组指针定义如下:
int total_arr(const int(*arr)[4], int n)
另外还有一种比较容易阅读的写法:
int total_arr(const int arr[][4], int n);
5、函数指针
如何声明函数指针:先写出函数原型,然后将函数名替换为(*pf),这样pf就是函数指针了。
double pam(int);
double(*pf)(int);
如何声明一个指向函数指针数组的指针呢?
double(*pa[3])(int);
使用typedef来定义一个函数指针
typedef const int * (*pf)(int x, int y);
如何使用:
int sum(const int *arr, int n)
{
int sum = 0;
for (size_t i = 0; i < n; i++)
sum += arr[i];
return sum;
}
typedef int(*pfunc)(const int *arr, int n);
pfunc f = sum;
int arr[5] = { 1, 2, 3, 4, 5};
int ret = f(arr, 5);
cout << ret << endl; // 15
6、C++内联函数
内联函数运行速度比常规函数稍快,但是代价是需要占用更多的内存,每次调用内联函数都会创建函数代码的一个副本。
应该有选择地使用内联函数:如果执行函数代码的时间比处理函数调用机制的时间长,则节省的时间只占整个过程的一小部分;如果代码执行的时间很短,则内联调用就可以节省函数调用机制的时间。
内联函数不能递归,如果函数定义占用多行(如果没有使用冗长的标识符),将其作为内联函数就不太合适。
inline int square(int x) { return x * x; }
7、将引用用作函数参数
如何创建引用变量:
int a = 10;
int &b = a;
cout << a << " " << b << endl; // 10 10
int c = 20;
b = c;
cout << a << " " << b << endl; // 20 20
c = 15;
cout << a << " " << b << endl; // 20 20
a = 11;
cout << a << " " << b << endl; // 11 11
引用必须在声明时将其初始化,不能像指针那样先声明再赋值。引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,将一直效忠于它。
int * const n2 = &n;
如果我们不想让b来修改a的值,我们可以添加const修饰符:
const int &b = a;
引用变量的主要用途是用作函数的形参:通过将引用变量用作参数,函数将使用原始数据,而不是其副本。如果只想让函数使用传递参数的信息,而不对这些信息进行修改,又想使用引用,应该是用常量引用:
int cube(const int &n);
编写类似于上述实例的函数(基本数值类型参数),应该使用按值传递的方式,不要使用按引用传递的方式,当数据比较大时,引用参数将会很有用。
引用传递的限制会更严格,比如 cube(x + 1)是不被允许的,因为x+1不是变量。
但是如果实参和引用参数不匹配,那么C++将生产临时变量(只有引用参数是const时才会产生),只有两种情况会生成临时变量:实参的类型正确,但不是左值;实参的类型不正确,但可以转换为正确的类型,这两种情况会创建一个正确类型的临时变量,然后使用转换后的实参来初始化形参。
什么是左值?左值参数是可被引用的数据对象,例如变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量和包含多项的表达式。
C语言中,左值最初指的是可以出现在赋值语句左边的实体。
int cube(const int &x)
{
return x * x*x;
}
int n = 2;
int & n1 = n;
int *n2 = &n;
int n3[3] = { 1, 2, 3 };
long n4 = 2;
int res = cube(n); // x is n
res = cube(n1); // x is n1 is n
res = cube(*n2); // x is *n2 is n
res = cube(n3[1]); // x is n3[1]
res = cube(n4); // x is tmp
res = cube(2); // x is tmp
res = cube(n + 1); // x is tmp
为什么要尽可能使用const?
a.使用const可以避免无意中修改数据的错误
b.使用const可以使函数能够处理const和非const实参,否则只能接受非const数据
c.使用const引用使函数能够正确生产并使用临时变量
将引用用于结构
引用非常适合适用于结构和类(主要是为了用于这些类型的,而不是基本的内置类型)
为什么要返回引用?传统返回机制与按值传递函数参数类似,返回引用速度会更快
struct people {
char name[10];
int age;
int height;
};
people& init(people & p) {
memcpy(p.name, "wang", 4);
p.age = 10;
p.height = 130;
return p;
}
返回引用时要注意,要避免返回函数终止时不在存在的内存单元引用。避免这种问题最简单的方法,返回一个作为参数传递给函数的引用。另一种方法是使用new来分配新的内存空间,并返回指向该内存空间的指针。
const people& init2() {
people *p = new people();
memcpy(p->name, "wang", 4);
p->age = 10;
p->height = 130;
return *p;
}
这种情况会比较容易忘了delete,后面会有unique_ptr来解决这个问题。
为何将const用于引用返回类型?常规返回类型是右值(不能通过地址访问的值),为什么常规函数返回值是右值呢?因为返回值位于临时单元中,执行下一句时,他们可能不再存在。
但是我们要使用引用返回值,但又不允许给init2()赋值这样的操作,只需要加上const,init2的返回值变成了一个不可修改的左值,赋值语句将不合法。
people a;
// init2() = a; // invalid
a = init2(); // valid
8、函数模板
函数模板是通用的函数描述,也就是说他们使用泛型来定义函数。由于类型是用参数表示的,所以模板特性有时也被称为参数化类型。
template <typename AnyType>
void mySwap(AnyType & a, AnyType & b)
{
AnyType tmp;
tmp = a;
a = b;
b = tmp;
}
int a = 10;
int b = 20;
mySwap(a, b);
mySwap<int>(a, b);
上述例子中调用模板函数mySwap可以指定类型,也可以不指定类型。如不指定类型,编译器或推断出类型。
不是所有的模板参数都必须是模板参数类型,我们也可以为某些参数指定固定类型:
template <typename T>
void mySwap(T & a, T & b);
template <typename T>
void mySwap(T * a, T * b, int n);
模板也有局限性,因为编写的模板函数很可能无法处理某些类型,解决这个问题有两种方案:一种是重载运算符,另一种解决方案是为特定类型提供具体化的模板定义。
模板具体化:显式具体化的意义是不要使用模板生成的定义,而应使用专门为job类型显示地定义地函数。
以下是两种等价的显式具体化的定义方式:
struct job
{
};
template<>
void mySwap<job>(const job & a, const job & b)
{
cout << "job" << endl;
}
template<>
void mySwap(const job & a, const job & b)
{
cout << "job" << endl;
}
job j, j1;
mySwap(j, j1); // job
对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及他们的重载版本。具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
void mySwap(const job & a, const job & b)
{
cout << "job b" << endl;
}
job j, j1;
mySwap(j, j1); // job b
模板实例化:在代码中包含函数模板本身并不会生成函数定义,他只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例,这种实例化方式被称为隐式实例化。以前只能通过隐式实例化来使用模板生成函数定义,现在还可以使用显示实例化。
以下是一个显式具体化的例子:
template void mySwap<job>(const job & a, const job & b);
该声明的意思是:使mySwap模板生成job类型的函数定义,生成过程中会进行模板具体化,调用时会调用该具体化函数。