C++学习笔记1
C++ 是作为 C 语言的面向对象版本而生的,因此它完全兼容 C 语言的特性。本文首先回顾了 C 语言中关于预处理器和指针的相关知识,而后介绍了引用、命名空间、重载等 C++ 特性。最后,本文总结了 static、const 等关键字的使用方法。
预处理器
C 语言中,我们习惯于使用宏定义来提高程序的执行效率。但宏定义的作用域为整个工程,不符合 C++ 中封装的思想。因此除非必要,还是应该使用类变量或函数来完成宏的功能(控制宏除外)。
常量
常量宏最为常见
#include <stdio.h>
#define PI 3.14 // #define Identifier Substitution
int main(int argc, char *argv[]) {
printf("%.2lf\n", PI);
return 0;
}
/* output:
* 3.14
*/
在大部分 PI
出现过的地方都会被 3.14
替换,除了以下情况
- 字符串字面量(两个引号之间)
- 另一个标识符内部(如
PICTURE
中的PI
不会被替换)
除了用户自定义的常量宏,预处理器定义了一些内置常量,主要用于 debug
cout << __FILE__ << endl; // main.cpp
cout << __LINE__ << endl; // 13
cout << __TIME__ << endl; // 10:48:07
cout << __DATE__ << endl; // Aug 29 2019
另外,在 gcc
中还支持如
cout << __func__ << endl; // main
cout << __FUNCTION__ << endl; // main
cout << __PRETTY_FUNCTION__ << endl; // int main(int, char**)
宏函数
当函数中存在一小段语句块被大量使用时,可以将其定义为宏函数(而不是一般的函数)以提高运行效率。例如
#include <stdio.h>
#define ADD(a,b) ((a)+(b))
int main(int argc, char *argv[]) {
printf("%d\n", ADD(1,2);
return 0;
}
/* output:
* 3
*/
如果一个宏函数要跨越多行,可以采用如下形式
#define LIST_HEAD(name, type) \
struct name { \
struct type *lh_first; /* first element */ \
}
行尾的 \
表示当前行没有结束,将下一行的内容拼接上来。注意最后一行不可以出现\
,除非在编程时确保宏函数后跟空行。在大多数多行宏函数中,需要采用 do-while
语句块来包裹
#define macro(a) do { \
expr1; \
expr2; \
} while (0)
for (int i = 0; i < N; i++)
macro(a);
等价于
for (int i = 0; i < N; i++)
do {
expr1;
expr2;
} while (0);
字符串操作
##
运算符可以用于字符串拼接
#define ENV_CREATE(x) \
{ \
extern u_char binary_##x##_start[]; \
extern u_int binary_##x##_size; \
env_create( \
binary_##x##_start, \
(u_int)binary_##x##_size); \
}
ENV_CREATE(user_icode);
上述代码将声明两个变量binary_user_icode_start
和binary_user_icode_size
,可见##
的作用即是将参数作为字符串强制拼接到函数体中。
与之相似,#
会将参数替换为用引号包裹的字符串。
#define STR(x) #x
void main(int argc, char *argv[]) {
cout << STR(hello) << endl;
return;
}
/* output:
* hello
*/
宏 STR
将其参数直接替换为 "hello"
输出。
控制宏
控制宏经常被用于控制头文件中代码被源文件包含的次数,避免重定义
#ifndef HEADER_H
#define HEADER_H
// ...
#endif /* HEADER_H */
在Visual Studio
中,这段代码的等价形式是
#pragma once
在某些情况下,可能需要同时支持 C 和 C++ 的编译方式,这时可以使用内置常量宏 __cplusplus
。当使用 C 方式编译时,这一常量不会被定义
#ifdef __cplusplus
// ...
#else
// ...
#endif /* __cplusplus */
指针
C 语言程序员对指针都是很熟悉的,但是很多人可能会忽视使用指针的规范
- 申请指针变量时赋初值
- 不需要指针时赋值为
NULL
- 申请内存后或使用指针前判断其有效性
- Never pass by value
- 不要使用浅拷贝
这些编程习惯可以有效的解决 fly pointer、memory leak、重复释放等问题。其中,最重要的是不要让两个指针指向同一片内存,也就是不要使用浅拷贝。尤其在并行程序中,浅拷贝所导致的问题可能有多种表现形式,十分难以查错。
回调函数
函数也需要保存在内存中,因此函数也有地址,可以被赋值给指针。指向函数的指针称为函数指针,通过指针调用的函数称为回调函数。函数指针变量的声明如下
// return_type (*p_name)(param_list);
int (*p_fun)(int, int);
使用时和函数调用类似
int fun(int, int);
p_fun = fun; // assignment
int c = (*p_fun)(1, 2); // call back
函数指针主要用于参数传递,可以实现模板
#include <stdio.h>
#include <stdlib.h>
int add(int a, int b) {
return a + b;
}
int mult(int a, int b) {
return a * b;
}
int template(int (*fun)(int, int), int a, int b) {
return (*fun)(a, b);
}
void main() {
printf("add result: %d\n", template(add, 2, 3));
printf("mult result: %d\n", template(mult, 2, 3));
}
输出
add result: 5
mult result: 6
引用
为了从语言层避免指针引发的一些问题,C++ 引入了引用。引用是更安全的指针,类似于变量的一个别名,他和指针有如下区别
- 引用不能为空:创建引用时就需要赋值
- 引用不能更改:一旦创建,不能指向其他对象
引用的定义和使用都和普通的变量一样
int a = 3;
int &b = a;
cout << b << endl;
甚至可以作为函数参数使用。例如用引用完成两个数的交换
void swap(int& a, int& b) {
int c = a;
a = b;
b = c;
}
类似的,引用也可以作为函数的返回值。只不过一般情况下我们不会返回引用,这是因为如果引用的对象定义在函数外,那么使用 inout
参数就可以了。而如果指针指向的对象定义在函数内,则一般是栈空间变量,在函数返回时会被销毁。但在以下的情境中,引用是可以作为函数返回值的
int& fun(void) {
static int a = 10;
return a;
}
void main(void) {
fun() = 20;
}
此时 fun
的返回值放在了等号左边,被称作左值。函数返回值很少作为左值,此处即为一个特例。
关键字
static
static
关键字用于描述静态函数或变量。
静态局部变量
静态局部变量会默认初始化为 0(未初始化的静态变量会被存放在 bss
段)。且该变量自声明起会一直存在,直到程序运行结束。但只有变量所在函数可以访问变量。
void fun(void) {
static int n;
cout << n++ << endl;
}
void main(int argc, char* argv[]) {
fun();
fun();
fun();
return;
}
输出
0
1
2
静态函数与静态全局变量
静态函数的使用和静态全局变量类似,只有当前文件内部可以访问。静态函数常常和 inline
关键字结合,用于在头文件中定义小型函数。
// header.h
static inline void fun(int, int) {
// ...
}
这样当多个源文件包含了 header.h
时,fun
实际以静态形式在这些文件中展开,并不会引发重定义的问题。
静态成员
在类中使用 static
关键字可以将成员变量声明为静态
// cat.h
class Cat
: public Animal {
public:
static int NLEG; // 声明静态成员变量
};
// cat.cpp
int Cat::NLEG = 4; // 需要在类外定义
int main(int argc, char *argv[]) {
Cat c;
cout << c.NLEG << endl; // 可以通过对象访问
cout << Cat::NLEG << endl; // 可以通过类名访问
return 0;
}
输出
4
4
静态成员变量在定义时分配存储空间,因此不能在头文件中定义。静态成员变量在类中只存在一份拷贝,且生命周期与程序相同。此外,静态成员变量既可以通过对象访问,也可以通过类名访问。
静态成员函数与静态成员变量类似,属于整个类而非某个特定对象。因此,静态成员函数中没有 this
指针,也无法调用非静态成员变量。
const
const
关键字用于定义一个不可变对象
const int i(5);
当 const
修饰指针变量时,涉及到指针本身和指针指向的变量两个变量,有时容易弄混究竟哪个是常量。助记方法:从右向左读声明语句。
int a = 3;
const int* p = &a; // p 是一个指向(*)整数(int)的常量(const)指针
int const *p = &a; // 同上
int * const p = &a; // p 是一个常量(const)指针,指向(*)整数(int)
const int* const p = &a;
常量引用指引用的对象不能通过引用被改变,但可以通过其他方式改变。
void main() {
int a = 3;
const int& ref = a;
cout << ref << endl;
a = 4;
cout << ref << endl;
}
输出
3
4
全局变量
被标记为 const
的全局变量默认具有 static
属性。要将其作用域变为全局,需要显式地将其声明为 extern
。
// animal.h
extern const int var;
// const int var = 10; // error LNK1120: 1 unresolved externals
不要忘记在声明时加上 const
关键字。
成员变量
// animal.h
class Animal {
const int spec;
public:
Animal(int spec);
};
被标记为 const
的成员变量必须通过初始化列表进行赋值。
// animal.cpp
Animal::Animal(int spec)
: spec(spec) {
// ...
}
成员函数
有时一个自定义类型的对象会被标记为 const
。这时,随意调用方法可能会改变对象状态,从而破坏 const
限制。因此,编译器会阻止对象调用任何非 const
方法
class Animal
{
int age;
public:
Animal(void) : age(0) {}
int getAge() const { return age; }
};
void main()
{
const Aniaml animal;
animal.getAge();
}
需要注意的是,修饰为 const
的方法
- 不能修改对象的成员变量(修饰为
mutable
的变量除外) - 不能调用非
const
方法
函数参数
函数参数可以视为局部变量。将函数参数声明为 const
不仅可以避免函数定义中更改参数内容,同时可以标明输入参数与输出参数。
因为 never pass by value
,自定义类型的函数参数往往是指针形式的。而这些指针中既包含输入参数,又包含输出参数,容易弄混。比较好的解决办法是将输入参数标为只读,这样用户只通过函数声明就可以知道哪些参数是输入参数,哪些是输出。
char* strcpy(char* dest, const char* src);
这是 C 库中 strcpy
函数的声明,很好的体现了 const
关键字在函数声明中的作用。
函数返回值
用 const
修饰函数返回值并不是很常见。这是因为
- 如果返回值是基础数据类型,不需要声明为常量
- 自定义数据类型通过
inout
参数返回
inline
inline
关键字用于声明内联函数。类似于 #define
定义的宏函数,内联函数也是为了提高程序运行效率而存在的,而且只适用于比较短小的函数。
当一个函数调用内联函数时,编译器将内联函数的代码拷贝到本函数中。这样就省去了运行时保存现场的开销,但是也造成了代码膨胀的问题。因此,使用 inline
关键字有一定限制
- 不能包含复杂的结构控制语句,如
for
、while
、switch
等 - 内联函数本身不能是递归函数
- 关键字需要与函数定义在一起才能生效(建议放在头文件)
同时需要注意
- 即使标识了关键字,编译器也不一定按内联函数处理
- 在类声明中如果定义了函数,则该函数默认内联
new & delete
new
和 delete
是 C++ 中定义的一对操作符,其作用分别对应于 C 中的 malloc
和 free
函数。
Cat* cat = new Cat();
delete cat;
等价于
Cat* cat = (Cat*)malloc(sizeof(Cat));
cat->Cat();
cat->~Cat();
free cat;
值得注意的是,在使用 new
和 delete
为数组分配空间时,同样需要配对使用
int *arr = new int[10];
delete [] arr;
如果仅仅使用 delete arr
将仅删除 arr
数组的第一个元素。
如果仅使用 new
分配了内存空间,而没有使用 delete
合理释放,将会导致内存泄露。因此,一般情况下我们需要确保一个语句块中的 new
和 delete
成对出现。