ctags
ctags(1) is old outdated BSD software, use something else instead, like maybe universal-ctags, or something even more recent? On the other hand, software that ships with the OS may have fewer third-party problems.
Still with me? ctags(1) parses text (C and other code) and emits a "tags" file. Other tools, text editors in particular, have features (or, bloat) to jump to the location of a tag. This allows one to quickly get to the definition of a function. An example may help.
$ grep eat_food monsters.c
static void eat_food(struct ani *other, struct app *app);
eat_food(self, app);
eat_food(struct ani *other, struct app *app)
eat_food(self, app);
eat_food(other, app);
$ ctags monsters.c
$ grep eat_food tags
eat_food monsters.c /^eat_food(struct ani *other, struct app *app)$/
After these commands, the `vi -t eat_food` command should open up on the "eat_food" function in "monsters.c", assuming no file changes that broke the regular expression. Within vi the ":ta eat_food" ex command does similar, or also the {control+]} key with the cursor sitting on the "e" of "eat_food".
An alternative
Another way to solve this is to use an ad-hoc query, something like
$ vi -c '/^eat_food' `grep -rl '^eat_food' .`
...
at the cost of a recursive grep being more expensive, especially as the source tree increases in size. A clever wrapper program might switch out the grep for git grep or other faster-than-grep options, though at some point maintaining a ctags-style file makes more sense. ctags is to "... grep -rl ..." much like locate(1) is to find(1): ctags has higher "capital" or setup costs, and is more likely to run into caching problems if the files are being edited a lot, though should help in larger and more established trees where there is less churn. It is pretty easy to setup a Makefile or whatever to rebuild the cache, though projects that use sub-directories will probably need something fancier.
tags: ${SRCS} {$HEADERS}
ctags ${SRCS} ${HEADERS}
Make things easy for regular expressions
The reason I format C code so the function name appears at the beginning of the line is to make function names easy and fast to find with a regular expression. Other C formatting styles may need fancier parsing and maybe not so fast regex on account of the function return values cluttering up the start of the line. In other languages you could look for a "^sub ..." or "(defun ..." prefix for a usually good enough expression, assuming there isn't documentation or or a heredoc that a tag regex incorrectly matches. In vi if there is a false match you could fix it, or just hit {n} for the next match, so probably not a big deal.
Format
$ head -1 tags | od -c
0000000 a n t _ u p d a t e \t m o n s t
0000020 e r s . c \t / ^ a n t _ u p d a
0000040 t e ( s t r u c t a n i * s
0000060 e l f , v o i d * a r g ) $
0000100 / \n
0000102
The tags format is simple: tag name, file, and regular expression, delimited by tabs. In theory you could build tags files for anything you frequently jump to, maybe sections in a TODO file? Probably these days you're supposed to use a language server protocol whatzit and not filthy regular expressions, but given it's taken me about 30 years of unix to get to ctags, we might expect to see me using LSP in 2046, or so, assuming survival of the Epochian Wars of 2038. And if ctags are good enough and do not take much CPU to generate or use, where's the problem?
Also I just now modified my vi to have a "tbuffer" or "tb" ex command so {yw:tb^M} does a ctags search using whatever {yw} or yank word put into the default buffer, or {"ayw:tb a^M} to use the "a" buffer instead. Some vi (or probably vim or neovim or whatever passes for vi these days) users claim they have never used anything besides the default buffer. This is fine if you've never had to juggle editing multiple strings, or do not use the nifty {@} command that executes the contents of a buffer. However instead of modifying your vi, instead use {control+]} which will jump to the tag that beings at the cursor, and then {``} jumps back, if it was a leap within the existing file, or otherwise {control+^} if the jump took you to a new file. These commands again are for the ex-vi on OpenBSD, or similar, so different vi implementations may vary. I've mapped {\e} (backslash, e) to the {control+^} command as control+shift+6 is difficult to type, and problematic key chords and finger pain is why I dropped emacs so many years ago now. vim looks like it has more cromulent ex-mode commands for navigating tags, but vim for me suffers from the "too much code" problem.
I do use marks, a lot, which are sort of like ad-hoc tags, and can be yet another (and even more succinct) way to get to a particular function or line. Mark with {me} on the "eat_food" function definition, then jump there with {'e} or {`e}. Use some mnemonic so you know what mark letter goes where, "e" for eat in this case, but marks are easy to setup and forget so tend to be short term use only. Marks can also be put in the middle of a function, while ctags is more for formal bits of structure that hopefully do not change much. Marks work for me as I'm often dealing with single file scripts so most everything is within that one file, and for anything elsewhere it's easy to get to that code using the same tools in a different tmux window. vi doesn't have any notable startup lag, and my ksh setup (or lack thereof) is also fast to start. If you have an editor or shell that takes seconds (or worse) to eventually get around to maybe being available, then using tools within those existing sessions will likely make more sense. There's that "high capital cost" thing again.
Tags Interface
With a fancy shell you could tab complete tag names, for even less typing on the command line, though a TUI (text user interface, e.g. a curses app) could easily be written to show, filter, and select a tag to get to. Someone has probably already written this, possibly several someones. Isn't this reinventing the IDE, poorly? Sort of, but an optional standalone program that forks/execs over to an editor (or execs to replace itself with the editor, or emits an editor command to run elsewhere) is rather different from wanting to use only the good bits of the monolith that is Xcode. In this way IDE are akin to large animals such as the Bruhathkayosaurus, while unix tools are more like the soil with its myriad of fungus that link up in who knows how many different ways.
"filesys/tagb" under my scripts repository is a prototype of such an tag selector interface.
I suppose the next thing would be to use ctags in anger, or maybe to actually read some ctags tutorials to see how various folks thought it was supposed to be used. ctags does seem to conflict with my editing style, a bit, which is to first think about what I'll be mangling, then to open up all the necessary files, e.g. `vi monsters.c rip.c` and then to use some maps to quickly move between the relevant files, and marks within the files for where I'm working.
map [b :previous
map [B :rewind
map ]b :next
The ctags "jumps to a different file" uses the "alternate edit" thing which is a swap with the previous file, and is distinct from the list of files given at startup, so if you tag jump twice to different files, say from "a.c" to "b.c", and then "b.c" to "c.c", only "b.c" and "c.c" will be active, and who knows what happened to "a.c". The ":args" command shows the original "a.c" (but where is it?), ":di sc" shows no background screens... maybe use ":edit a.c" to get back to where you started, but it might be faster to quit and restart vi, especially if "a.c" is something long and annoying to type. Maybe split screen editing could help here, but I've more or less disabled that, and I've pretty much disabled all the commands to create split screens or background windows, as I forever hit those Upper Case commands by dragging on the shift key and get {:W} and not the {:w} I wanted.
$ cat -b {a,b,c}.c
1 int main(int argc, char *argv[]) {
2 bbb(foo);
3 }
1 void
2 bbb(void)
3 {
4 ccc(foo);
5 }
1 void
2 ccc(void)
3 {
4 42;
5 }
$ ctags {a,b,c}.c
$ vi a.c
Oh, {:rewind} gets you back to "a.c", but ":previous" yells at you about there not being a previous file as you did not ":next" to "b.c" and then "c.c" but rather probably an ":edit" command was used internally.
My version of vi does cull a lot of messages when switching files, and tends to autowrite instead of "warning! unwritten bytes!!" and "hey! here's a new long filename!" prompts. This leads to smoother file changes, at the cost of maybe ignoring various messages and shoving bytes to disk more often than necessary, maybe overwriting something if the locks did not get setup aright. So I'm more "back and forth, then use marks to get around within each file" than "jump around randomly with tags", but maybe tag jumping can work out if I give it more time and a fair shake.
Since vi starts quickly, a different set of files to edit is just "quit, escape, k, edit the vi command, enter"—the shell also uses vi mode. Also different sessions of vi can be run, so for example in this tmux window is a blog post, and in a different xterm is the "tagb" code, and other tmux windows can be used to test the code, etc. Focus follows mouse, so getting between the big old xterms is quick and easy. Some have claimed that unix is the IDE.
tbuffer
Revisiting the tbuffer ex command I invented, one use might be to put several function names into several buffers—the "buffer" is another ex command I invented; it puts the given text into the named buffer; without it, you would have to {"eyw}, {"fyw}, {"iyw} with the function name under the cursor for each named buffer, which can be annoying to arrange.
:bu e eat_food
:bu f food_that_heals
:bu i is_food
Then, {:tb e}, {:tb f}, and {:tb i} should jump you to the appropriate function. This gives you a catalog of arbitrary tags, as opposed to the stack offered in the original implementation. This will be limited by the number of buffers available, what you can remember, and what else you use the buffers for.