Android工程师 为啥要学习c/c++呢?
首要仍是本身遇到瓶颈了吧, 学习下c的常识,扩展下自己编写so的能力,不然许多框架的确也是看不明白,特别是涉及到跨端的组件,不明白点底层是真的难搞
爽性从头学一遍c的常识,顺便把开源库中喜欢用到的pthread,mmap,文件流 等linux 操作也过一遍
基础环境建立
我是mac, 大部分情况下 下个clion 就能够直接写c言语了, 可是考虑到Android 底层是linux,并不是mac的osx, 所以理想情况下 仍是期望 咱们编写的c程序能直接跑在 linux上,
所以基础环境的建立 便是根据docker 来建立一个linux ,然后让咱们的clion 直接远程到这个docker上即可
#
# Build and run:
# docker build -t clion/centos7-cpp-env:0.1 -f Dockerfile.centos7-cpp-env .
# docker run -d --cap-add sys_ptrace -p127.0.0.1:2222:22 clion/centos7-cpp-env:0.1
# ssh-keygen -f "$HOME/.ssh/known_hosts" -R "[localhost]:2222"
#
# stop:
# docker stop clion_remote_env
#
# ssh credentials (test user):
# user@password
FROM centos:7
RUN sed -e 's|^mirrorlist=|#mirrorlist=|g' \
-e 's|^#baseurl=http://mirror.centos.org|baseurl=https://mirrors.tuna.tsinghua.edu.cn|g' \
-i.bak \
/etc/yum.repos.d/CentOS-*.repo
RUN yum -y update \
&& yum -y install openssh-server \
make \
autoconf \
automake \
locales-all \
dos2unix \
ninja-build \
build-essential \
gcc \
gcc-c++ \
gdb \
clang \
cmake \
rsync \
tar \
python \
&& yum clean all
RUN ssh-keygen -A
RUN ( \
echo 'LogLevel DEBUG2'; \
echo 'PermitRootLogin yes'; \
echo 'PasswordAuthentication yes'; \
echo 'Subsystem sftp /usr/libexec/openssh/sftp-server'; \
) > /etc/ssh/sshd_config_test_clion
RUN useradd -m user \
&& yes password | passwd user
CMD ["/usr/sbin/sshd", "-D", "-e", "-f", "/etc/ssh/sshd_config_test_clion"]
去clion 那儿设置一下 toolchians
然后再编译一下
再看下 咱们引证的stdio 头文件,这儿能看出来 引证的是 linux上的头文件了, 有爱好的能够看下 这个头文件linux的完成和 mac上的 头文件完成有何不同
这儿有一点要注意,clion默认的cmake版别比较高,你要连centos7 ,默认的cmake版别低,会编译失败
这儿 咱们只需设置一下 版别号为centos的cmake 版别号就行了
别的便是假如一个project 下 你有多个main函数入口 需要额外设置一下cmake
cmake_minimum_required(VERSION 2.8.12.2)
project(cstudy C)
set(CMAKE_C_STANDARD 11)
#一个project 下面假如有多个main函数入口 需要设置多个add_executable()
add_executable(cstudy date_types.c)
add_executable(cstudy2 func_test.c)
说实话 写多了java kotlin js 啥的,你会发现 cmake 这东西是真的蠢。。。。
函数的变长参数
#include <stdio.h>
#include <stdarg.h>
void HandleVarargs(int arg_count,...) {
// 用于获取变长参数
va_list args;
// 开端遍历
va_start(args, arg_count);
int j;
for ( j = 0; j < arg_count; ++j) {
// 取出对应参数
int arg = va_arg(args, int);
printf("%d: %d \n", j, arg);
}
// 完毕遍历
va_end(args);
}
int main() {
HandleVarargs(4,1,2,5,6);
return 0;
}
在c中 写个变长参数的函数 真的累。。。 还要写这种模版代码
最坑的是,你在调用这个参数的时分,榜首个参数 还有必要得是你参数的个数。。。
宏
头文件
这个include 其实便是一个宏,和你直接用define 其实便是相同的
本质上便是在你编译的时分 把其他 函数/变量 直接导入过来,好让编译器知道 你这儿应该怎么调用一个函数
就这么简略
自己写头文件
这个也挺麻烦的,其实便是当你想对外供给一个功用,做一个模块的时分 就会用到这个东西,相比java 的无脑,c里边实在是太麻烦了
能够新建include和src 2个途径,一个放完成,一个放头文件
然后咱们新建一个头文件:
//
// Created by 吴越 on 2023/2/18.
//
#ifndef CSTUDY_INCLUDE_FAC_H_
#define CSTUDY_INCLUDE_FAC_H_
int sum(int x,int y);
#endif //CSTUDY_INCLUDE_FAC_H_
再新建一个对应的完成 .c 文件
//
// Created by 吴越 on 2023/2/18.
//
#include "../include/fac.h"
int sum(int x,int y){
return x+y;
}
此刻不要忘掉cmake文件要改一下
add_executable(cstudy3 hong_test.c src/fac.c)
最终调用一下
#include <stdio.h>
#include "include/fac.h"
int main() {
printf("sum: %d \n", sum(3, 5));
return 0;
}
就这么简略!
<> 和 ““ 的差异
前面的代码能够看到 咱们都是用的”” 来引证的头文件, 那么有么有办法 像引证系统头文件相同就用括号呢? 其实也是的能够的, 可是要去cmake 里边添加一行代码
include_directories("include")
这个技巧要把握一下,不然许多开源代码 你会搞晕的, 换句话说 拿到开源的代码 仍是先看一下 cmake文件吧。
宏函数
#include <stdio.h>
#define MAX(a, b) a>b?a:b
int main() {
printf("max : %d \n", MAX(1, 3));
printf("max : %d \n", MAX(1, MAX(4,3)));
return 0;
}
看下履行成果:
是不是和预想中的不太相同?
宏函数和普通函数仍是有差异的,他便是直接在编译的时分替换掉的
稍微改一下:
#define MAX(a, b) (a) >(b) ? (a) :(b)
应该就ok了
宏本质上便是 代码替换,和函数是不相同的
个人认为,宏函数比函数好的地方就在于 他没有类型约束
宏函数如何换行
#include <stdio.h>
#define MAX(a, b) (a) >(b) ? (a) :(b)
#define is_hex_char(c) \
((c)>='0'&& (c)<='9') || \
((c)>='a' && (c)<='f')
int main() {
printf("max : %d \n", MAX(1, 3));
printf("max : %d \n", MAX(1, MAX(4, 3)));
printf("is hex : %d \n", is_hex_char('a'));
return 0;
}
条件编译
看下之前写的代码:
这些黄色的代码代表啥意思?作用是什么?
其实你想一下,这个头文件里边 声明晰一个函数
假如 你的程序很大的情况下, 假如有多处地方都include了这个头文件,等于你是不是有多个 同名的函数?
那编译不是会报错嘛,这儿的黄色代码 就代表是这个意思
条件编译 在某些时分 挺有用的,比方你想本地编译的时分 打印调试信息 ,正式版别不打印
#include <stdio.h>
void dump(){
#ifdef DEBUG
printf("debug info");
#endif
}
int main(){
dump();
return 0;
}
然后在cmake中写一下:
add_executable(cstudy5 macro_test2.c)
target_compile_definitions(cstudy5 PUBLIC DEBUG)
跑一下:
很有意思,你明明没有界说debug 这个宏,可是运行的成果 却打印了调试信息, 这个其实也是cmake 在发挥作用
一般咱们能够使用条件编译来判别 咱们到底是在c仍是cpp,在mac,仍是在windows 仍是在linux的环境中
=printf 主动换行
供给了2个版别的完成,显着宏的完成更简练一些
#include <stdio.h>
#include <stdarg.h>
#define PRINTFLINE(format, ...) printf(format"\n",##__VA_ARGS__)
void Printf(const char *format, ...) {
va_list args;
va_start(args, format);
vprintf(format, args);
printf("\n");
va_end(args);
}
int main() {
Printf("hello");
PRINTFLINE("world");
return 0;
}
有的人觉得 c言语 打印个变量也太麻烦了,还要写百分号,有没有更简略的
#define PRINT_INT(value) PRINTFLINE(#value": %d",value)
还有更进一步的:
咱们期望打印的时分 能够主动把咱们的行号,所属的文件 ,文件名都打印出来,这样打印调试信息的就很清晰了 大型工程的时分特别有用
#define PRINTFLINE(format, ...) printf("("__FILE__":%d) %s : "format"\n",__LINE__, __FUNCTION__, ##__VA_ARGS__)
字符串
字符串基本常识
c 言语中的字符串 有必要以 null 也便是\0 为结尾
#include <stdio.h>
#include <ioprint.h>
int main(){
char string[5]="wuyue";
char string2[6]="wuyue";
PRINTFLINE("string: %s",string);
PRINTFLINE("string2: %s",string2);
return 0;
}
来看下履行成果:
榜首行的成果肯定是不对的,其实问题便是 没有多拓荒一个元素方位 用来放\0 这一点和其他言语又不相同
指针
指针的只读
这儿 能够看到 编译报错了, cp的地址是不能改的,可是值能够修正
反过来看一下:
这儿便是 地址能够改,可是值不能修正
还有一种写法 能够看出来,这儿甚至连值都不能改了,
首要仍是看 *的方位 总结起来便是:
const 假如润饰的是指针,则地址不能修正 const 假如润饰的是值,则值不能修正
其实便是看const的左边有没有*
左右值问题
先看下 下面这段代码:
#include <stdio.h>
#include <ioprint.h>
int main() {
int array[4] = {0};
int *pa= array;
*pa=2;
*(pa++)=3;
*(pa+2)=4;
PRINT_INT_ARRAY(array,4);
return 0;
}
履行成果:
要搞清楚一个概念,等号 右边 永远是取出来的值,而等号左边 永远是一块地址空间
榜首次是把2 这个值 赋给 方位为0的元素
第二次是 pa这个指针的方位 的值 也便是方位为0的元素 改为3,然后 这个pa的指针 挪后了一位
第三次 便是pa这个指针 再挪两位
指针参数作为返回值
这个务必要搞清楚了,许多开源项目都是很多运用这个技巧
比方说 咱们前面求和的这个函数
假如你在main函数中调用他 会产生什么呢?
首先在sum这个函数把成果计算出来之后, 是先把这个成果 从内存中拷贝到cpu的寄存器中
第二步: 再从cpu的寄存器中将这个值拷贝到内存中
这儿是不是就涉及到两次拷贝了? 假如你这个函数很复杂,是个结构体,这个开销仍是有的,
所以许多时分 咱们会这么写:
void sum2(int x, int y, int *result) {
*result = x + y;
}
函数的最终1个或者n个参数 作为接纳函数的返回值,能够完成函数多返回值,而且省略内存拷贝的开销
动态分配内存
别的言语 动态声明1个数组是能够的, 可是在c言语 就比较麻烦了, 需要像下面的程序相同 才能够办到
#include <stdio.h>
#include <ioprint.h>
#include <malloc.h>
int main() {
int size = 10;
int *array = malloc(sizeof(int) * size);
int i;
for (i = 0; i < size; ++i) {
array[i]=i;
}
PRINT_INT_ARRAY(array,size)
free(array);
return 0;
}
这儿要注意2点,榜首 malloc和free 要成对呈现
第二 malloc分配的内存块 最好要榜首时间初始化, 由于malloc是在堆区上分配的一块内存,你不知道这块内存上是什么值,所以你申请完以后 榜首时间要做初始化
注意了,这儿有一个大坑, 比方咱们想把这个 初始化的过程作为一个函数 来便利调用, 许多人会这么写:
void InitIntArray(int *a, int size, int defaultValue) {
a = malloc(sizeof(int) * size);
int i;
for (i = 0; i < size; ++i) {
a[i] = defaultValue;
}
}
int main() {
int size = 10;
int *a;
InitIntArray(a,size,0);
PRINT_INT_ARRAY(a, size)
free(a);
return 0;
}
看上去好像这段代码没什么问题 可是你运行起来就会报错了, 来看下正确的代码 应该怎么写:
void InitIntArray2(int **aparams, int size, int defaultValue) {
*aparams = malloc(sizeof(int) * size);
int i;
for (i = 0; i < size; ++i) {
(*aparams)[i] = defaultValue;
}
}
int main() {
int size = 10;
int *a;
InitIntArray2(&a,size,0);
PRINT_INT_ARRAY(a, size)
free(a);
return 0;
}
能够体会一下 这2个写法的差异, 咱们首先看一下 榜首个写法 为什么不对,
在c言语中,函数都是值传递,什么是值传递?
也便是关于榜首种写法来说,
虽然你在办法内部 成功malloc了一块内存,可是指向这块内存的是 你函数的参数,并不是main函数中的 指a
一定要切记,函数都是值传递的。
那第二种写法为什么正确?
首先你传入的是一个指针的地址
也便是说 aParams = &a
那么 *aParams 就等于 a
别的也能够关注下 calloc这个函数, 这是主动初始化值的,还有一个realloc 从头分配一段内存, 一般能够用于动态扩展数组的巨细
// 主动初始化
int *b = calloc(size, sizeof(int));
// 在之前的基础上,从头拓荒一段空间,等于是扩展了
b = realloc(b, size * 2);
if (b != NULL) {
PRINTFLINE("分配成功");
PRINT_INT_ARRAY(b, size);
} else{
PRINTFLINE("分配失败");
}
return 0;
函数指针
在c言语中 是能够界说一个函数指针的,很意外吧 , 比方说 上面那个小节的比如,还能够这么写
void (*func)(int **aparams, int size, int defaultValue) = &InitIntArray2;
size = 30;
func(&a, size, 30);
PRINT_INT_ARRAY(a, size);
这儿真的很容易利诱 能够看下 下面的几种写法
// f1是一个函数,这个函数的返回值 是一个int * 指针
int *f1(int,double );
// f2是一个函数指针,指向一个 参数为int,double 返回值是int的 函数
int (*f2) (int,double );
// f3和f2 相同,只不过返回值 是一个 int * 的函数
int *(*f3) (int,double );
// 函数的指针能够界说数组,可是函数不能界说数组
int (*f5[]) (int,double);
当然还能够指定别号
// 界说这个函数指针的别号
typedef int (*Func)(int, int);
int Add(int x, int y) {
return x + y;
}
然后去调用他
Func func_1 = &Add;
PRINTFLINE("result : %d" ,func_1(3, 4));
这儿有点绕,可是不要紧 咱们能够在觉得看不明白代码的时分 仿制一下类型 到下面的网站 让他给你答案即可
到这儿看一下 到底是啥类型