배열
배열은 개발에서 흔히 사용되는 데이터 구조입니다. 물론 우리는 배열이 컨테이너와 동일하다는 것을 알고 있지만 값과 문자를 저장할 수 있을 뿐만 아니라 함수의 항목과 구조의 데이터를 저장합니다.
typedef struct _value { int val_1; int val_2; }VALUE; typedef struct _table { char index; char data[100]; int (*UniSetFunc)(VALUE*); }TABLE; int add(VALUE *val ) { int temp = 0; temp = val->val_1 + val->val_2; return temp; } TABLE page[10] { {“ First section ”, “abcdefghijklmn”, add}, {“Second section”, “opqrstuvwxyz”, NULL} }; int main() { VALUE AddValue; AddValue.val_1 = 2; AddValue.val_2 = 4; int result = 0; result = page[0]-> UniSetFunc(&AddValue); printf(“The Result of add is %d\n”, result); return 0; }
이때 배열은 Python 언어의 사전과 유사한 구조로 변환되므로 이후 개발 및 활용은 물론 추가 업그레이드 및 유지 관리가 용이합니다.
코드 분석: 우선 함수의 이름이 함수의 입력 주소로 사용될 수 있다는 것을 알고 있습니다(배열의 주소를 나타내는 배열의 이름과 유사). TABLE 구조는 int (* UniSetFunc)(VALUE*) 멤버 함수를 정의합니다. 여기서 UniSetFunc는 함수의 입력 매개변수로 사용되며 VALUE*는 TABLE 유형 배열 페이지를 포함합니다. add 함수를 간접적으로 호출하고 AddValue.val_1 및 AddValue.val_1 두 숫자의 합을 구현하기 위해 페이지 [0]-> UniSetFunc(&AddValue)에서 호출할 수 있는 함수의 항목 주소와 구조의 데이터입니다. 값을 추가합니다.
메모리 적중률 문제:
코드 연산의 효율성을 높이기 위해서는 메모리 공간을 최대한 활용하고 메모리 영역에 지속적으로 접근해야 합니다.
메모리에서 배열이 저장되는 위치는 전체 영역이며, 정의된 배열 array[2][2]에 대해 array[0][0] 주소를 가정합니다. 가 0x04030이면 array[0][1], array[1][0], array[1][1]의 주소는 각각 0x04031, 0x04032, 0x04033이므로 메모리 적중률이 향상됩니다. 점프 접근보다는 메모리 공간 영역에 대한 지속적인 접근 다음으로 행렬 곱셈 문제를 살펴보겠습니다.
for (int i = 0; i < 2; ++i) { for (int j = 0; j < 2; ++j) { for (int k = 0; k < 3; ++k) { matrix[i][j] += matrix1[i][k] * matrix2[k][j]; } } }
위 코드는 행렬1과 행렬2를 곱한 후 행렬에 대입하는 일반적으로 사용되는 방법입니다. 즉, 행렬1 행렬을 사용하여 행렬2의 열 벡터를 곱한 행 벡터를 구하는 방법입니다. 그런 다음 이를 행렬에 할당하므로 메모리의 행렬 저장 구조와 관련하여 행렬2에 액세스할 때 연속 액세스를 사용하지 않으므로 메모리 적중률이 낮다는 것을 분명히 알 수 있습니다. 다음으로 메모리 적중률이 높은 방법을 살펴보겠습니다.
for (int i = 0; i < 2; ++i) { for (int k = 0; k < 3; ++k) { for (int j = 0; j < 2; ++j) { matrix[i][j] += matrix1[i][k] * matrix2[k][j]; } } }
코드에서는 두 번째 for 루프와 세 번째 for 루프의 위치만 교환하고 다른 부분은 변경되지 않은 것을 볼 수 있습니다. 그러나 메모리 적중률이 크게 향상되었습니다. 행렬 곱셈의 목적은 행렬1과 행렬2 내부의 요소들을 순차적으로 곱한 다음 이를 누적하는 것입니다. 이러한 방식으로 행렬1과 행렬2에 액세스할 때 메모리 누락이 발생하지 않으므로 메모리 적중 확률이 향상됩니다.
휘발성, 상수, 정적의 관계:
const 키워드는 상수 키워드이며, 이 키워드가 작용하는 양은 상수입니다. 예를 들어 const int value = 12; 상수는 프로그램에 의해 변경될 수 없습니다. const 키워드는 프로그램이 실수로 고정 상수를 변경하는 것을 방지하기 위해 제때에 const 키워드를 추가해야 합니다. const 키워드가 상수에 작용하는 경우 상수를 직접 초기화해야 합니다. 전체 프로그램이 실행되는 동안에는 변경할 수 없으므로 즉시 초기화해야 합니다. 예: const int 값 = 12는 정확하지만 const int value; value = 12; 이 구문은 잘못되었습니다! 다음으로 좀 더 어려운 문제인 상수 포인터와 포인터 상수를 살펴보겠습니다. 먼저 코드를 살펴보겠습니다.
#define SWITCH 1 int main() { int val_1 = 5; int val_2 = 10; const int *p1 = &val_1; int const *p2 = &val_1; int *const p3 = &val_1; #ifdef SWITCH // This is a switch *p1 = 20; *p2 = 21; *p3 = 22; #endif #ifndef SWITCH p1 = &val_2; p2 = &val_2; p3 = &val_2; #endif printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); return 0; }
cygwin 컴파일러에서 실행하면 다음 오류를 볼 수 있습니다.
그림에서 포인터 p1과 p2는 포인터 상수로서 val_1의 값만 읽을 수 있다는 것을 분명히 알 수 있습니다. 즉, 변경할 수 없습니다. 변수의 내용이므로 *p1 = 20; *p2 = 21; (#ifdef SWITCH ... #endif는 매크로 스위치인 조건부 컴파일입니다.) 그런 다음 #define SWITCH 1 문을 주석 처리합니다. 이때 두 번째 코드 블록이 실행되고 결과는 다음과 같습니다.
오류를 보면 알 수 있습니다. p3은 상수 포인터이므로 고정된 주소만 가리킬 수 있고 가리키는 방향을 변경할 수 없으므로 p3 = &val_2의 연산이 잘못되었으므로 올바른 코드는 다음과 같습니다.
<🎜; >int main() { int val_1 = 5; int val_2 = 10; const int *p1 = &val_1; int const *p2 = &val_1; int *const p3 = &val_1; printf("Frist\n"); printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); p1 = &val_2; p2 = &val_2; *p3 = 22; printf("Second\n"); printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); return 0; }
const int * const p = &a;或int const *const p = &a; 在定义函数的时候,若该入口参数在程序执行的过程中不希望被改变,则一定要将该形参用const来修饰,一来这样可以防止该段程序将其改变,二来对于形参而言,一个无论是否是const修饰的实参都可以将其传入const形的形参,而一个const形的实参是无法传入非const形的形参中,所以为了使编译不出错在定义函数的时候,一定要将不希望被改变的量用const关键字来修饰。
Static关键字为静态关键字,它的作用是将作用的变量存入内存中,而非存入寄存器中(即将变量存入堆中而非栈中),并且该作用的变量仅保存最近一次获取的值。接下来我们来看一段代码。
void countfun () { static int count = 0; ++count; printf(“This is %d number, enter into this function !\n”, count ); } int main() { for (int i = 0; i < 5; ++i) { countfun(); } return 0; }
这段代码的运行结果如下:
而若将除去static关键字,则运行的结果如下:
由此我们可以清楚的看出,static作用的变量count只会存入当前的结果,因此循环调用countfun( )函数的时候并没有从新将count变量置为0,而是保存了前一次的值。
Static关键字在项目中的应用是很广泛的,它不仅仅有上述所介绍的特点,同时若想要定义的全局变量在整个工程中仅在当前.C文件中有效时,也应该将这个全局变量用static来修饰,这样在其他的文件中是无法访问这个变量,从而降低了模块间的耦合度,提高了模块的内聚性,防止其他文件将其改变,从而更加的安全。
volatile关键字在嵌入式领域中是十分重要的一个关键字,尤其是在与硬件相关或多线程的编程中更为重要。volatile关键字修饰的变量说明它是可以随时发生改变的,我们不希望编译器去优化某些代码的时候,需要将这个变量用volatile关键字来修饰,从而程序每次访问该变量的时候是直接从内存中提取出来,而不是从临时的寄存器中将该变量的副本给提取出来利用!例如当我们想要实现某个中断处理时,其用来做判断条件的标记位则应该用volatile来修饰,这样当这个中断在别的地方被触发的时候就可以被实时的检测到,不至于由于优化而忽略中断。接下来我们看一段代码:
int main() { volatile int i = 10; int a = i; printf(“i = %d\n”, a); __asm { mov dword ptr[ebp-4], 0x10 } int b = i; printf(“i = %d\n”, b); return 0; }
此程序输出结果为i = 10;i = 16; 若将volatile关键字去掉,则结果为i = 10;i = 10;
即不加关键字会将汇编代码忽略掉,所以为了防止代码优化以及可以及时检测到外部程序对该变量的改变,我们必须将该变量加上volatile关键字。我们知道volatile关键字表征该量是易变的,const关键字代表该量是常量不能改变,那么volatile与const是否可以一起修饰同一个量呢,是肯定的,例如在硬件编程中ROM所存储的数据是不允许用户改变的,即指向该数据的指针必须为常量指针(const int *p = &ram_data),然而开发商却可以将其意外的改变,为了防止ROM的内容被意外的改变时,而用户程序没有及时的发现,必须将该量用volatile修饰,所以应这样定义该指针(volatile const int *p = &rom_data)。
位运算
在数字解码与编码的过程中,位运算的操作是司空见惯的事,同时位运算在提高程序的性能方面也独占鳌头,因此位运算操作是必需要深入了解的问题。
在乘法以及除法的操作中我可以使用未运行来提高代码的质量,例如:a = a * 16;这种操作完全可以替换为:a = a << 4;我们知道左移一位相当于将原数乘以2,左移N位则相当于乘以2^N,前提是在没有发生溢出的情况下;故上例即相当于将数a左移4位,对于某些乘以非2的整数幂情况,如 a = a * 9;则可以改写为a = (a << 3) + a; 同理右移相当于除以2的整数幂,当然以上所有情况都是在没有发生数据溢出的情况下,因此位运算操作要格外的小心,否则极有可能发生出错的情况。
在数据类型转换的过程中也需要做位运算操作,例如我们想将一个unsigned short类型的数据存入unsigned char类型的数组中,就需要进行位运算,首先分析知道unsigned short占用16个字节,unsigned char占用8个字节,想要将大字节的数据存入小字节,必须要对大字节进行分割,即将高8位与低8为分离开来分别存放,来看实现代码:
unsigned char * DrawCompo_Pj_BT_Change(unsigned short *subarray) { unsigned char temp[500]; (void)_sys_memset(&temp, 0x00, sizeof(temp) ); unsigned short i = 0; while (subarray[i] != 0x0000) { if( (subarray[i] & 0xff00) == 0x0000) { temp[i++] = (unsigned char)(subarray[i] & 0x00ff); } else { temp[i] = (unsigned char)( (subarray[i] & 0xff00) >> 8); temp[i++] = (unsigned char)(subarray[i] & 0x00ff); } } temp[i] = '\0'; return temp; }
temp[i] = (unsigned char)( (subarray[i] & 0xff00) >> 8);即取subarray[i]数据的高8位,temp[i++] = (unsigned char)(subarray[i] & 0x00ff);取低8位。这样就可以实现将高字节的数据完整的存入到低字节中。
位运算还可以用来判断变量的符号,我们知道对于一个有符号的变量,其最高位为其符号位,故检查改变的最高位即可知道该变量为正还是为负。看一段代码:
int main() { short test_data = -12; if (test_data & 0xF000) { printf("This number is negative "); } else { printf("This number is positive "); } return 0; }
对于想要交换两个数的值的时候,通常我们的做法如下:
void swap(int &data1, int &data2) { int temp = 0; temp = data1; data1 = data2; data2 = temp; }
这样的代码比较简单易懂,然而美中不足的是它会产生一个临时变量temp,接下来我们用位运算来重写这个程序;
void swap(int &data1, int &data2) { data1 = data1 ^ data2; data2 = data1 ^ data2; data1 = data1 ^ data2; }
从上面的代码我们可以看出少了一个临时变量,同时也加快了代码的运行效率。
尾递归:
递归调用给我们带来了很多方便,也简化了代码,使程序看起来更加的简洁和明了,但递归调用也通常伴随着一个潜在的危险:出栈,接下来我们来看一个常见的递归。
int factorial(int n) { if (n < 1) { return 1; } else { return factorial(n-1)*n; } }
通常在求一个数的阶乘的时候我们习惯于采用上述方法,然而分析来看当输入的n值较大时,factorial(n-1)*n所计算的值会不断的压入堆栈,生成很多的临时变量,等待下一个的值的确定才得以计算,然而在内存中堆栈的大小是固定的,当输入的n值很大时,极有可能产生堆栈溢出!因此,有一个好的方法解决这种问题即尾递归调用。接下来我们来看这种强大的算法。
int factorial(int n, int m) { if (n < 2) { return m; } else { factorial(n-1, n*m); } }
从代码中可以看出,通过引入一个新的参数m来存放每次递归所产生的值,这样就避免了每次递归都要进行压栈操作,也就不会产生堆栈溢出的现象,而且普通的递归每次递归只能等待下一次递归得到的结果后才能继续运算,而尾递归每次执行都会进行运算,一次循环执行完毕即可得到结果,其时间复杂度为O(n);